- 발행일
한 숙소에 가격 필드가 여덟 개 — 최소~최대 표시를 맞추던 일
한 숙소에 가격 필드가 여덟 개
이 글은 2026년 9월에 당시 코드를 다시 보며 정리한 것입니다. 저장소는 퇴사 전에 스냅샷으로 보관한 것이라 커밋 이력이 없고, 경력기술서와 당시 쓴 이력서에 적은 담당 범위와 코드를 대조해 썼습니다. 날짜는 코드에 남은 시기 단서 기준입니다.
숙박 큐레이션 플랫폼의 프런트 팀은 넷, 백엔드 팀도 넷이었습니다. 2023년 12월에 쓴 이력서에는 제 담당 역할이 "가격, 홍보에 관련된 프런트엔드 개발"로 적혀 있어요. 가격 표시는 그중 앞쪽입니다.
숙소 목록 카드에는 가격이 한 줄 들어갑니다. "₩180,000 "처럼요. 할인이 걸린 숙소는 원가에 줄을 긋고 할인가를 앞에 세워야 했습니다. 경력기술서에는 이렇게 적었어요. "고객 화면에는 최소최대 가격을 명시하고 할인 옵션이 있으면 할인가와 기본가를 시각적으로 구분했다."
그 한 줄을 만들려고 타입 파일을 열었더니 가격 필드가 이렇게 있었습니다.
new_price_min: number;
new_price_max: number;
new_min_price: number;
new_max_price: number;
price_min: number;
price_max: number;
max_price: number;
min_price: number;
네 쌍, 여덟 개. price_min과 min_price가 다른 값이고, 거기에 new_가 붙은 쌍이 또 둘입니다. 화면 한 줄에 셋을 넣어야 하는데 어느 셋인지가 첫 문제였어요.
네 쌍이 어디서 오는가
이름만으로는 갈리지 않습니다. 이 글을 쓰며 서버 직렬화기를 열었고, 그때도 열어 봤을 텐데 기록은 없어요. Rails 쪽 코드는 백엔드 팀 것이라 여기서는 "서버가 이렇게 준다"까지만 적습니다.
| 쌍 | 출처 | 뜻 |
|---|---|---|
price_min / price_max | DB 컬럼 그대로 | 숙소에 등록된 기본가 범위. 원화 |
min_price / max_price | 직렬화기가 price_min·price_max를 접속 언어의 통화로 환산 | 같은 값의 환율 적용판 |
new_price_min / new_price_max | 2023년 8~9월에 추가된 컬럼 | 예약 가능 기간의 실제 판매가를 매일 계산해 심은 값 |
new_min_price / new_max_price | 위 둘의 환율 적용판 | 화면이 쓰는 쌍 |
그러니까 실제로는 두 가지 값(기본가, 계산된 판매가)이 각각 원화판과 환산판으로 갈려 넷이 된 겁니다. 마이그레이션 파일 날짜가 new_price_min은 8월 17일, new_price_max는 9월 1일이에요. 두 주 간격으로 따로 들어온 걸 보면 처음엔 최솟값만 심을 생각이었다가 최대가 필요해진 것으로 읽힙니다. 이유는 코드에 남아 있지 않습니다.
최대라는 이름에 최솟값이 들어간다
서버 서비스는 숙소의 방마다, 오늘부터 예약 가능 마지막 날까지 하루씩 가격을 계산합니다. 그리고 이렇게 마무리해요.
min_price = min_price_list.min if min_price_list != nil
max_price = max_price_list.min if max_price_list != nil
max_price_list에서도 .min을 뽑습니다. min_price_list에는 할인 적용가가, max_price_list에는 할인 전 예약가가 쌓이는데, 둘 다 기간 안의 최솟값을 취해요. 결과적으로 new_price_max는 "최대 가격"이 아니라 "할인 전 가격 중 가장 싼 날"입니다.
이걸 몰랐다면 화면에서 new_max_price를 "최대"로 읽고 "18만~25만"처럼 범위로 그렸을 겁니다. 실제로는 원가와 할인가 한 쌍이었어요. 이름을 믿지 않고 계산 코드를 읽어야 맞는 화면이 나오는 구조였습니다. 그때 읽고 골랐는지, 누가 알려 줬는지는 남아 있지 않아요.
화면은 어느 쌍을 믿는가
정리한 규칙은 컴포넌트 하나에 뒀습니다.
if (!minPrice || !maxPrice || minPrice >= originalPrice) {
return <>{displayPrice(locale, originalPrice)} ~</>;
}
return (
<>
<del className="strike">{displayPrice(locale, maxPrice)}</del>
{displayPrice(locale, minPrice)} ~
세 값을 받습니다. minPrice에 new_min_price(할인가), maxPrice에 new_max_price(할인 전가), originalPrice에 min_price(기본가)를 넣어요. 계산값이 없거나 0이면 기본가를 그대로 보여 주고, 할인가가 기본가보다 싸지 않으면 할인 표시를 하지 않습니다. 줄을 긋는 대상은 maxPrice이고 앞에 서는 건 minPrice예요.
이 컴포넌트를 부르는 곳이 셋입니다. 숙소 목록, 메인 슬라이더, 마이페이지 찜 목록. 세 곳이 전부 같은 세 필드를 같은 순서로 넘깁니다. 규칙을 한 곳에 두려고 했던 건 맞는데, 그 셋이 전부 같은 필드를 넘기는지는 컴포넌트가 강제하지 않아요. prop 이름이 minPrice·maxPrice·originalPrice라 호출하는 쪽이 new_min_price를 minPrice에 넣어야 한다는 걸 알아야 합니다.
통화 표시는 언어로 갈랐습니다.
export const displayPriceByLocale = (locale, price) => {
switch (locale) {
case "en":
return `US$ ${getPriceDisplayStr(price)}`;
case "ja":
return `¥${getPriceDisplayStr(price)}`;
default:
return `₩${getPriceDisplayStr(price)}`;
환산은 서버가 하고 기호만 프런트가 붙입니다. 그래서 화면은 늘 _price로 끝나는 환산판을 써야 하고, price_로 시작하는 원화판을 쓰면 일본어 페이지에 원화 숫자가 엔 기호로 나갑니다. 이 구분이 이름에 안 실려 있어서 필드를 고를 때마다 직렬화기를 다시 봐야 했어요.
필터 슬라이더의 최소·최대는 다른 문제였다
같은 "최소·최대"라도 검색 필터의 가격 슬라이더는 다른 일이었습니다. 0부터 100까지 만원 단위 슬라이더와 직접 입력 칸 둘을 한 상태로 합쳐야 했어요.
const handleFocusOut = () => {
if (inputMax < inputMin) {
setRangePrice({ min: inputMax - 1, max: inputMax });
setInputMin(inputMax - 1);
}
입력 칸에서 최소가 최대보다 크면 최소를 최대 바로 아래로 끌어내립니다. 쿼리는 min * 10000-max * 10000으로 만들고, 0~100 그대로면 빈 문자열이라 "조건 없음"이 됩니다. 이 컴포넌트가 findstay와 picks 두 폴더에 거의 같은 내용으로 두 벌 있어요. 두 벌은 슬라이더·입력 칸·쿼리 조립 로직이 같은데, 파일 길이가 171줄과 258줄로 다르고 diff -w로 219줄이 갈립니다. 공통 로직을 뽑지 않고 한쪽에 살을 붙인 모양이에요.
검증
- 날짜 근거: Rails 마이그레이션
add_new_price_min_to_place(2023-08-17)와add_new_price_max_to_place(2023-09-01). 글의 날짜는 뒤엣것입니다. - 필드 여덟 개는
types/models/places.ts에서, 환산 규칙은place_new_serializer.rb의new_min_price·new_max_price메서드에서 확인했습니다. max_price_list.min은new_price_service.rb에서 확인했습니다. 이 서비스는 백엔드 팀 코드이고, 제가 쓴 부분은 그 값을 받는 프런트입니다.- 호출처 셋(
findstay-list.tsx·main-swiper.tsx·mypage/likestay.tsx)이 같은 세 필드를 넘기는 것을 grep으로 셌습니다. 스냅샷 시점 기준이에요. - 문서 두 개와 대조한 결과입니다.
| 항목 | 경력기술서(2024-03) | 이력서(2023-12) |
|---|---|---|
| 담당 범위 | §5 "고객 화면에 최소~최대 가격 명시, 할인가·기본가 구분" — 일치 | "가격, 홍보에 관련된 프런트엔드 개발" — 일치 |
| A/B로 표시 방식 비교 | §5에 있음 — 이 저장소에서 근거 못 찾음 | 언급 없음 |
| 팀 규모 | 적지 않음 | 프런트 4명·백엔드 4명 — 가격 컴포넌트를 혼자 짰는지는 확인 불가 |
남은 것 · 한계
- 프런트가 넷이었습니다. 가격 표시가 제 담당 역할이었다는 건 이력서에 있지만, 이 컴포넌트와 호출처 셋을 전부 제가 썼는지는 이력이 없어 확인할 수 없습니다.
- 커밋 이력이 없어 순서를 모릅니다. 컬럼이 두 주 간격으로 들어온 이유, 화면이 언제
new_쌍으로 갈아탔는지는 마이그레이션 날짜로만 추정합니다. new_price_max라는 이름은 그대로 남았습니다. 값은 "할인 전 최저가"인데 이름은 "최대"예요. 프런트에서 prop 이름을originalPrice·maxPrice로 바꿔 받는 것으로 넘어갔고, 서버 컬럼 이름을 고치자고 제안한 기록은 없습니다.- 필드 선택을 강제하는 장치가 없습니다. 호출처 셋이 같은 필드를 넘기는 건 관례일 뿐이라, 넷째 호출처가
price_min을 넘겨도 타입은 통과합니다. - 필터 컴포넌트 두 벌은 그대로 두 벌입니다.
- 3년 뒤 SDMS에서 같은 표준데이터를 두 화면이 198건과 134건으로 세고 있었다를 겪으면서 "같은 값을 두 이름으로 부르면 화면마다 갈린다"를 다시 만났습니다.
관련 글: 숙박 큐레이션 플랫폼 프런트엔드 2022–2024 · 같은 표준데이터를 두 화면이 198건과 134건으로 세고 있었다 · Figma 44프레임을 코드와 대조하기 · 메인 화면의 판독 박스는 서버 설정 셋이 그린다 · 프로모션마다 지원 언어를 두고, 안 되는 언어면 뒤로 보냈다