Skip to content
Go back

Chrome 페이지 번역이 Next.js 다국어 사이트를 망가뜨리는 이유

배경

다국어를 지원하는 Next.js 사이트를 운영하다가 QA에서 이상한 버그 리포트가 올라왔다. “언어를 한국어로 바꿨는데 일부 텍스트가 영어로 남아있다”, “특정 페이지에서 레이아웃이 깨진다” — 재현 조건은 Chrome의 페이지 번역 기능이 켜져 있을 때였다.

처음에는 단순한 번역 오작동으로 생각했는데, 파고들어 보니 Chrome 번역 엔진과 React의 렌더링 방식이 근본적으로 충돌하는 문제였다.

Chrome 페이지 번역이 하는 일

Chrome의 페이지 번역은 브라우저 레벨에서 동작한다. <html lang="en">인 페이지를 한국어 사용자가 열면, Chrome이 자동으로 번역을 제안하고, 사용자가 수락하면 Real DOM의 텍스트 노드를 직접 교체한다.

핵심은 이 과정이 JavaScript를 거치지 않는다는 점이다. React도, Next.js도, i18n 라이브러리도 모르는 사이에 DOM이 바뀐다.

[Chrome 번역 엔진]
  → Real DOM의 텍스트 노드를 직접 탐색
  → 텍스트를 Google Translate API로 번역
  → 번역된 텍스트로 DOM 노드를 교체
  → <font> 태그로 번역된 텍스트를 감싸기도 함

React는 Virtual DOM과 Real DOM이 항상 일치한다고 가정한다. Chrome 번역은 이 가정을 깨뜨린다.

충돌 지점 1 — Hydration Mismatch

Next.js는 서버에서 HTML을 렌더링(SSR)하고, 클라이언트에서 React가 이 HTML에 이벤트 핸들러를 붙이는 hydration 과정을 거친다. Hydration 시 React는 서버에서 만든 DOM과 클라이언트의 Virtual DOM을 비교한다.

문제는 이 비교가 일어나기 전에 Chrome 번역이 개입하는 경우다.

1. 서버 → HTML 응답 (lang="en", 영어 텍스트)
2. 브라우저가 HTML을 파싱하여 Real DOM 생성
3. Chrome 번역 → Real DOM의 텍스트를 한국어로 교체  ← 여기
4. React hydration → Virtual DOM(영어)과 Real DOM(한국어) 불일치 감지
5. React가 Real DOM을 Virtual DOM 기준으로 복원 → 번역이 풀림

사용자 입장에서는 번역이 잠깐 적용됐다가 사라지는 깜빡임(flicker) 현상으로 보인다.

충돌 지점 2 — 부분 재렌더링

Hydration 이후에도 문제는 계속된다. React가 state 변경으로 컴포넌트를 부분 재렌더링하면, Virtual DOM diff 결과를 Real DOM에 반영한다. 이때 Chrome이 번역해둔 텍스트와 React의 Virtual DOM이 달라서 예측할 수 없는 결과가 나온다.

Chrome 번역 후 Real DOM:
  <p><font class="goog-text-highlight">안녕하세요</font></p>

React Virtual DOM:
  <p>Hello</p>

React 재렌더링 시:
  → diff: "텍스트 노드가 다르다" or "자식 구조가 다르다"
  → 결과: 일부만 복원되거나 <font> 태그가 남거나 레이아웃 깨짐

특히 Chrome이 번역 시 삽입하는 <font> 태그가 문제다. React는 이 태그의 존재를 모르기 때문에, DOM 트리 구조 자체가 불일치하게 된다.

<html lang> 속성의 역할

Chrome 번역 트리거의 핵심은 <html lang> 속성이다. 이 값이 사용자의 브라우저 언어와 다르면 번역을 제안한다.

다국어 사이트에서 흔히 발생하는 시나리오:

  1. 서버가 Accept-Language 헤더를 보고 lang="en" 페이지를 응답
  2. 사용자가 사이트 내 언어 설정에서 한국어로 변경
  3. i18n 라이브러리가 텍스트를 한국어로 교체하지만, <html lang>은 여전히 "en"
  4. Chrome이 “이 페이지는 영어입니다. 번역할까요?” 제안
  5. 사용자가 수락 → 이미 한국어인 텍스트를 다시 번역 시도 → 충돌

해결의 핵심은 <html lang> 속성을 i18n 언어 변경과 동기화하는 것이었다. 언어가 바뀔 때 document.documentElement.lang도 함께 업데이트하면, Chrome이 “이미 사용자 언어와 같다”고 판단하여 번역을 제안하지 않는다.

SSR 환경에서 언어를 결정하는 순서도 중요하다. 서버는 첫 요청에서 사용자의 언어를 알아야 올바른 <html lang>을 내려줄 수 있다.

1순위: Cookie (사용자가 명시적으로 선택한 언어)
2순위: Accept-Language 헤더 (브라우저 기본 언어)
3순위: 기본값 (ko)

Cookie를 1순위로 두는 이유는 Accept-Language와 사이트 언어 설정이 다를 수 있기 때문이다. 영어 브라우저를 쓰면서 사이트는 한국어로 보고 싶은 사용자가 있다. Cookie에 선택한 언어를 저장하고, SSR 시 이 값을 읽어 <html lang>을 설정하면 Chrome 번역 트리거도 방지되고 hydration mismatch도 없다.

처음에는 언어 설정을 localStorage에 저장했다. 클라이언트에서 읽기 쉬우니까. 그런데 SSR에서 localStorage를 읽을 수 없다는 기본적인 문제가 있었다.

서버가 언어를 모르니까 기본값(한국어)으로 HTML을 내려보내고, 클라이언트에서 localStorage를 읽어 영어로 바꾸면 — 전체 페이지가 한국어에서 영어로 깜빡이며 전환되는 현상이 발생했다. 여기에 Chrome 번역까지 끼어들면 삼중 충돌이었다.

Cookie로 바꾸니까 서버에서도 언어를 알 수 있어서 SSR 시점부터 올바른 언어로 렌더링되고, <html lang>도 정확하게 설정되고, Chrome 번역 트리거도 방지됐다.

회고

프레임워크가 관리하는 DOM을 외부에서 건드리면, 프레임워크의 모든 가정이 무너진다.

Chrome 번역은 React의 통제 밖에서 DOM을 수정하는 대표적인 사례다. 비슷한 문제가 브라우저 확장 프로그램, 접근성 도구, 자동 완성 기능에서도 발생할 수 있다. React가 “DOM의 진실은 나한테 있다”고 생각하는 한, 외부 DOM 조작과의 충돌은 피할 수 없다.

다국어 사이트를 만든다면 <html lang> 동기화는 선택이 아니라 필수다. 이 한 줄의 속성이 Chrome 번역 트리거, SEO lang 태그, 스크린 리더의 발음 엔진까지 영향을 미친다.


Share this post on:

Comments


Next Post
AI 에이전트 하네스 개발기 #5 — 실제 프로젝트에 적용해보니