Skip to content

fix: 언어 전환 첫 클릭이 하이드레이션 전이면 무시됨 #152

Description

@Binz-collab

증상

언어 설정 화면에서 언어를 고르면 첫 클릭이 무시되는 경우가 있습니다. 잠시 뒤 다시 누르면 정상 동작합니다.

화면은 이미 다 그려져 있어서 사용자 입장에서는 "눌렀는데 아무 일도 안 일어난다"로 보입니다.

재현 조건

PR #147 검증 중 두 차례 재현했습니다.

  1. 언어를 전환하면 window.location.assign으로 전체 페이지 로드가 발생합니다.
  2. 로드 직후 곧바로 다른 언어를 누르면 무시됩니다.
  3. 특히 /settings/language로 진입해 서버가 /en/settings/language로 307 리다이렉트한 직후에 더 잘 재현됐습니다.

원인 (확인 완료 — 처음 적었던 것과 다릅니다)

처음에는 "HTML은 즉시 그려지지만 하이드레이션 전까지 <button onClick> 핸들러가 없다"로 봤습니다. 아닙니다.

프로덕션 /settings/language의 HTML을 직접 받아 확인했습니다.

{ "hasLanguageLabels": false, "buttonCount": 0, "anchorCount": 0,
  "ariaHiddenSkeleton": true }

언어 목록이 서버 응답에 아예 없습니다. 버튼도 링크도 0개입니다. 목록은 스타일 프로브(useSettingsStyleReady)가 풀린 뒤에야 그려지는데, 그건 하이드레이션이 끝난 다음입니다. 즉 핸들러 없는 목록을 누를 수 있는 순간 자체가 존재하지 않습니다.

진짜 원인은 스켈레톤입니다.

  • SettingsLanguageSkeleton이 헤더와 행 자리를 그대로 흉내 내서 화면이 다 그려진 것처럼 보입니다.
  • 그런데 pointer-events: none이라 클릭이 막힌 층에 걸려 사라집니다.
  • 언어를 바꾸면 전체 페이지가 다시 로드되므로 그 직후에 이 구간을 다시 만납니다.
  • 307이 한 번 더 끼면 스타일 청크가 더 늦게 도착해 구간이 길어집니다. → 재현율이 올라간 이유.

재현 조건 세 가지와 전부 맞아떨어집니다.

검증된 해법

아래 셋을 함께 적용하면 해소됩니다. 구현·검증까지 마쳤고 CI도 통과했으나, 진행 순서 조정으로 이번에는 반영하지 않습니다. 코드는 브랜치 fix/language-switch-before-hydration과 닫힌 PR #182에 남아 있습니다.

1. 언어 목록을 처음부터 낸다 (스켈레톤 뒤에 두지 않음)

이 화면의 인라인 폴백 스타일이 이미 클래스와 같은 배치를 담고 있어(스타일 청크가 늦을 때 쓰려고 만들어 둔 것), 스켈레톤 없이도 화면이 튀지 않습니다.

2. 행을 링크로 바꾼다

로케일은 이미 URL이 유일한 원천이라 언어 전환은 다른 주소로 가는 것 이상이 아닙니다. 접근성도 좋아집니다.

3. 선호 기록을 서버로 옮긴다

링크만으로는 스크립트가 돌지 않아 쿠키를 쓸 수 없습니다. /set-language/{로케일}{돌아갈 자리}로 들어온 요청만 사용자의 선택으로 보고 기록한 뒤 그 자리로 돌려보냅니다. 로케일 URL을 그냥 방문한 것은 여전히 선호가 아닙니다(#147 유지).

함정 1 — 쿼리 파라미터로는 안 됩니다

처음에 ?set-lang=ko 표시로 만들었더니 기본 로케일(한국어) 선택이 통째로 무시됐습니다.

/settings/language?set-lang=ja      200 OK   <- 가드가 안 돔
/en/settings/language?set-lang=ko   307      <- 여긴 돔

프리렌더된 정적 파일이 서버 핸들러보다 먼저 응답하기 때문입니다. 이 레포는 vite.config.ts의 AUTH_DEPENDENT_PATHS 주석에서 이미 같은 함정을 경고하고 있습니다. 기본 로케일 자리야말로 프리렌더 대상이라 예외를 둘 수 없어, 전용 경로여야 합니다.

함정 2 — 가드 순서

선택을 가장 먼저 처리하지 않으면, 기본 로케일을 고른 경우 아직 살아 있는 낡은 쿠키가 사용자를 방금 고른 것과 다른 곳으로 끌고 갑니다.

함정 3 — normalizeLocale을 경로에 쓰면 안 됩니다

normalizeLocale은 Accept-Language의 en-US를 받으려고 접두사 일치를 씁니다. 경로 조각에 그대로 대면 english → en, javascript → ja, korean → ko로 통과합니다. 경로 조각 전용 파서가 따로 필요합니다.

검증 방법

E2E를 JS를 끈 브라우저로 돌리면 하이드레이션 전 상황이 그대로 재현됩니다. 스켈레톤 게이트를 되돌리면 3건 중 2건이 깨지는 것까지 확인했습니다.

영향 범위

  • apps/web/src/routes/settings.language.tsx
  • apps/web/src/features/settings/ui/LanguageSettingList.tsx
  • apps/web/src/shared/i18n/server-locale-guard.ts
  • apps/web/src/shared/store/language.ts

남은 별건

홈 헤더의 언어 드롭다운은 목록을 여는 것부터 스크립트가 필요해 같은 방식으로는 풀리지 않습니다(role="listbox" 안의 <a>는 접근성상으로도 어긋납니다). 홈도 프리렌더 대상이라 트리거 버튼의 첫 탭이 하이드레이션 전이면 같은 식으로 사라질 수 있습니다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions