발행일

프로모션마다 지원 언어를 두고, 안 되는 언어면 뒤로 보냈다 — 404 대신 모달과 back

프로모션마다 지원 언어를 두고, 안 되는 언어면 뒤로 보냈다

이 글은 2026년 9월에 당시 코드를 다시 보며 정리한 것입니다. 저장소는 퇴사 전에 스냅샷으로 보관한 것이라 커밋 이력이 없고, 경력기술서와 당시 쓴 이력서에 적은 담당 범위와 코드를 대조해 썼습니다. 날짜는 코드에 남은 시기 단서 기준입니다.

프로모션은 당시 이력서에 적은 제 담당 역할 "가격·홍보 관련 프런트엔드 개발"의 홍보 쪽이고, 일본어는 경력기술서에 제 담당으로 적혀 있습니다. 이 화면은 그 둘이 겹치는 자리예요.

프로모션 페이지는 언어별로 사정이 달랐습니다. 국내 카드사 제휴처럼 한국어로만 뜻이 있는 것, 해외 사용자를 겨냥해 세 언어로 다 만든 것. 그런데 주소는 언어를 안 가려요. 영어 화면을 보던 사람이 한국어 전용 프로모션 링크를 열면 무엇을 보여 줄지 정해야 했습니다.

서버가 준 것: 지원 언어 목록

Rails 마이그레이션에 그 시작이 남아 있습니다.

class AddLocalesToPromotion < ActiveRecord::Migration[6.1]
  def up
    add_column :promotions, :locales, :string
    execute "UPDATE promotions SET locales =
      case
      when is_en = 1 then 'ko,en'
      else 'ko'
      end
    "
  end

그 전에는 is_en이라는 불리언 하나였어요. 영어를 지원하느냐 아니냐. 일본어가 들어오면서 불리언으로는 안 되니 locales라는 콤마 문자열이 됐고, 기존 값은 그 불리언에서 옮겨 채웠습니다. 같은 날 프로모션 테이블에 title_ja·contents_ja 같은 일본어 컬럼 열 개가 붙었고요. 이 마이그레이션은 서버 쪽 작업이라 제 손인지는 알 수 없습니다. 프런트가 그 값을 어떻게 썼는지가 제 범위예요.

화면이 한 것: 검사하고, 모달, 그리고 back

  const { locale } = useRouter();
  const [isSupportLocale, setIsSupportLocale] = useState<boolean>(
    checkSupportLocale()
  );

  useEffect(() => {
    setIsSupportLocale(checkSupportLocale());
  }, [locale]);

  function checkSupportLocale() {
    return promotionData?.locales?.includes(locale);
  }

"ko,en" 문자열에 현재 언어가 들어 있는지 includes로 봅니다. 콤마를 쪼개지 않고 문자열 포함으로 검사해요. 언어 코드가 두 글자라 겹칠 일이 없어서 통했고, 그 점을 믿고 쓴 건지 그냥 짧아서 쓴 건지는 남아 있지 않습니다.

지원하지 않으면 BlockEnAlert가 뜹니다.

  const onClose = () => {
    document.body.classList.remove("scroll_lock");
    closeAlertHandler();
    router.back();
  };

닫으면 router.back(). 컴포넌트 이름은 영어 시절에 지어진 BlockEnAlert인데 문구는 t("localeNotSupport")로 세 언어를 다 냅니다. 한국어는 "한국어가 지원되지 않는 페이지 입니다", 영어는 "This menu does not support English", 일본어는 "日本語に対応していないページです"였어요.

선택지는 이 정도였다고 봅니다.

방법포기하는 것
404페이지는 있는데 없다고 말하게 됨. 링크를 받은 사람은 링크가 깨진 줄 앎
한국어로 강제 표시영어 화면 한가운데 한국어 페이지가 뜸
모달 + back사용자가 어디로 갈지는 못 정해 줌. 온 길로 돌려보낼 뿐

셋째를 골랐습니다. "이 언어로는 없다"는 사실을 그 언어로 말하고, 온 길로 돌려보내는 것. 이벤트 페이지(pages/events/[identifier].tsx)도 같은 컴포넌트와 같은 검사를 씁니다. 다만 거기는 필드 이름이 locale(단수)이라 서버 응답 모양이 둘로 갈려 있었어요.

딜 목록이 이어받은 무한 스크롤

같은 프로모션 화면 아래에는 딜 목록이 있습니다. 경력기술서 §2에서 "프리오더·프로모션 페이지로 확장 가능한지도 확인했다"고 적은 그 자리예요.

  const { data, setSize, isLoading, isValidating, error } = useSWRInfinite(
    infiniteKeyGenerator(dealApiUrl, query.per, query.locale),
    fetcherWithError,
    {
      errorRetryCount: 0,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      revalidateFirstPage: false,
      dedupingInterval: 0,
      fallbackData: [],
    }
  );

useSWRInfinite에 언어를 쿼리로 실어 페이지 키를 만들고, useSWRInfiniteScroll 훅이 IntersectionObserversetSize를 올립니다. 확장은 됐어요. 그런데 옵션을 보면 revalidateOnFocus: false, revalidateOnReconnect: false입니다. 경력기술서 §2는 SWR을 고른 이유로 "캐시 무효화·포커스 리페칭이 기본값"이라고 적었는데, 프로모션 딜 목록에서는 그 기본값을 전부 껐습니다. 딜은 자주 바뀌지 않으니 끄는 게 맞았을 수 있지만, 그러면 고른 이유였던 것이 여기서는 이유가 아니에요.

딜 카드에는 getDdayCount(deal.start_at)로 D-day를 붙이고, 혜택 문구는 deal.translation[router.locale].benefit을 줄바꿈으로 쪼개 목록으로 그립니다. 번역 객체를 언어 키로 직접 뽑는 방식이라 서버가 그 언어를 안 주면 undefined가 되고, 그래서 위의 지원 언어 검사가 앞에 서 있어야 했습니다.

검증

  • pages/promotions/[code].tsx·pages/events/[identifier].tsx·components/layout/block-en-alert.tsx·components/promotion/promotion-deal.tsx·common/hooks/useSWRInfiniteScroll.ts를 스냅샷에서 읽었습니다. 세 언어 문구는 리소스 파일에서 확인했습니다.
  • 날짜 근거: Rails db/migrate/20230224060958_add_locales_to_promotion과 같은 날 add_japanese_fields_to_promotion.
  • 당시 이력서(2023-12) 대조: 담당 역할 "가격·홍보"와 일치. 프런트 4명 팀이라 이 파일의 모든 줄이 제 것이라고는 말할 수 없습니다.
  • 경력기술서 대조: §2 "프로모션으로 확장" 일치, §2 "포커스 리페칭이 기본값이라 채택"은 이 화면 옵션과 불일치, §5 "프로모션과 프리오더의 데이터 구조 통합"은 이 스냅샷에서 확인하지 못했습니다.
  • 그때 어느 언어·어느 프로모션으로 모달을 확인했는지는 기록이 없습니다.

남은 것 · 한계

  • includes는 문자열 검사입니다. 언어 코드가 두 글자라 통했지, 콤마로 쪼개 비교한 게 아니에요. 언어가 늘어 코드가 겹치면 잘못 통과합니다.
  • 이벤트는 locale, 프로모션은 locales. 같은 검사가 두 필드 이름을 봅니다. 서버 응답을 프런트가 맞춘 결과인데 왜 안 통일했는지는 남아 있지 않습니다.
  • router.back()은 온 길이 없으면 아무 데도 안 갑니다. 링크를 직접 열어 들어온 사람은 모달을 닫아도 그 자리예요. 이 경우를 처리한 코드는 없습니다.
  • 이벤트 페이지는 훅보다 앞에 return null이 있습니다. 데이터가 없으면 훅을 안 부르고 나가는 구조라 React 훅 규칙에 어긋나요. 그때 문제가 됐는지는 기록이 없고, 이 스냅샷에 그렇게 남아 있다는 것만 적어 둡니다.
  • 1년 반 뒤 TripTune에서 무한 스크롤을 다시 고를 때는 이 화면의 "기본값을 껐다"는 사실을 떠올리지 못했습니다. 고른 이유와 실제 설정이 어긋난 것을 이 글을 쓰며 처음 봤어요.

관련 글: 숙박 큐레이션 플랫폼 프런트엔드 2022–2024 · 무한 스크롤 비교 · React Infinite Scroll 구현하기 · 일본어는 도메인, 영어는 경로 · Google Optimize가 사라진 뒤 미들웨어로 A/B를 돌렸다