Google 번역을 켰더니 React 앱이 오류 화면으로 바뀐 이유
한국어, 영어, 일본어, 중국어를 직접 제공하는 React 랜딩 페이지에서 예상하지 못한 문제가 생겼습니다. Chrome이 띄운 "이 페이지를 번역하시겠습니까?" 제안을 수락하면 번역이 시작되는 듯하다가, 잠시 뒤 사이트의 "일시적인 오류가 발생했습니다" 화면으로 바뀌었습니다.
처음에는 Google 번역 서비스가 실패하면서 보여 주는 화면처럼 보였습니다. 하지만 그 문구는 Google 것이 아니었습니다. 프로젝트의 오류 경계가 출력하는 문구였습니다. 즉 번역 자체의 실패라기보다, 번역 이후 React 렌더링이 예외를 던지고 있었습니다.
오류 화면의 주인부터 확인했다
문제 화면에 나온 문장을 코드 전체에서 검색했습니다. 같은 문구가 Next.js의 error.tsx와 global-error.tsx에 있었습니다.
이 한 번의 검색으로 범위가 크게 줄었습니다.
- Google 번역기의 자체 오류 화면이 아니다.
- 서버가 잘못된 HTML을 보낸 것도 아니다.
- 번역이 페이지를 바꾼 뒤, 클라이언트의 React 트리에서 예외가 발생했다.
- 그 예외를 애플리케이션의 오류 경계가 잡아 대체 화면을 그렸다.
화면에 보이는 에러 문구가 누구의 것인지 먼저 확인하면 외부 서비스 문제와 애플리케이션 문제를 빠르게 나눌 수 있습니다.
Google 번역은 글자만 바꾸지 않는다
Chrome의 페이지 번역은 별도의 번역 스크립트를 페이지에 주입합니다. 이 스크립트는 화면에 있는 텍스트를 찾고, 번역 결과를 보여 주기 위해 실제 DOM의 텍스트 노드를 교체하거나 새 요소로 감쌉니다.
문제는 React 역시 같은 DOM을 자신이 관리한다고 가정한다는 점입니다. React는 이전 렌더 결과를 기준으로 다음 변경분을 계산합니다. 그런데 그 사이 Google 번역이 자식 노드를 교체해 버리면 React가 기억하는 구조와 브라우저의 실제 구조가 달라집니다.
그 상태에서 상태 변경, 애니메이션, 반응형 분기 같은 이유로 컴포넌트가 다시 렌더링되면 React는 이미 사라진 노드를 제거하거나, 더는 존재하지 않는 노드 앞에 새 노드를 넣으려 할 수 있습니다. 이때 브라우저에서는 다음 계열의 DOM 예외가 날 수 있습니다.
Failed to execute 'removeChild' on 'Node'
The node to be removed is not a child of this nodeReact 저장소에도 Google Translate가 DOM을 수정한 뒤 removeChild 예외가 발생하는 같은 종류의 사례가 오래전부터 보고돼 있습니다.
처음에는 잘 보이다가 나중에 깨지는 이유
이 문제는 일반적인 하이드레이션 오류와 시점이 다릅니다.
- 서버 HTML이 내려오고 React 하이드레이션이 정상적으로 끝납니다.
- 사용자가 번역을 실행합니다.
- Google 번역이 이미 만들어진 DOM을 바꿉니다.
- 이후 React 컴포넌트가 다시 렌더링됩니다.
- React가 기대한 노드와 실제 노드가 달라 DOM 조작이 실패합니다.
그래서 첫 화면은 멀쩡하고 번역된 문장도 잠깐 보일 수 있습니다. 동적인 다시 렌더링이 일어나는 순간에야 오류 경계로 들어갑니다.
suppressHydrationWarning으로 해결되지 않는다
React나 Next.js 프로젝트에서 DOM 불일치를 보면 suppressHydrationWarning을 먼저 떠올리기 쉽습니다. 하지만 이 옵션은 서버 HTML과 최초 클라이언트 렌더 결과가 다른 하이드레이션 경고를 제한하는 용도입니다.
이번 문제는 하이드레이션이 끝난 뒤 제3자 스크립트가 DOM을 바꿔서 발생합니다. 경고를 숨겨도 실제 노드 구조는 돌아오지 않으므로 해결책이 아닙니다.
인터넷에는 Node.prototype.removeChild나 insertBefore를 덮어써 예외를 무시하는 우회 코드도 있습니다. 이런 방식은 페이지 전체의 DOM 동작을 바꾸고, 이미 어긋난 React 상태를 감춘 채 실행을 계속하게 합니다. 다른 라이브러리까지 영향을 받을 수 있어 운영 코드에는 넣지 않았습니다.
텍스트를 항상 별도 요소로 감싸고 동적인 텍스트 노드를 줄이면 충돌 가능성을 낮출 수는 있습니다. 하지만 사이트 전체에서 외부 번역기가 어떤 DOM을 만들지 통제하기는 어렵습니다.
자체 다국어 사이트라면 번역 대상에서 제외한다
이 사이트는 이미 언어별 경로와 번역 문구를 직접 제공합니다. 한국어 /, 영어 /en, 일본어 /ja, 중국어 /zh처럼 사용자가 원하는 언어를 고를 수 있습니다.
이 경우 브라우저 자동 번역은 필요한 기능이 아니라 React DOM을 건드리는 두 번째 번역 계층입니다. 그래서 루트 레이아웃에서 페이지 번역을 명시적으로 제외했습니다.
<html lang={locale} translate="no">
<head>
<meta name="google" content="notranslate" />
</head>
<body>{children}</body>
</html>두 설정의 역할은 조금 다릅니다.
<meta name="google" content="notranslate">는 Google에 페이지 번역 제안을 하지 말라는 신호입니다.translate="no"는 해당 요소와 자손의 텍스트를 번역하지 말라는 HTML 표준 속성입니다.lang={locale}는 그대로 유지합니다. 검색엔진과 보조 기술이 현재 문서 언어를 정확히 알아야 하기 때문입니다.
적용 뒤에는 각 언어의 빌드 결과 HTML에 lang, translate="no", notranslate가 모두 들어갔는지 확인했습니다.
모든 사이트에서 번역을 막아야 하는 것은 아니다
이 선택에는 조건이 있습니다. 자체 번역이 없는 단일 언어 사이트라면 브라우저 번역은 해외 사용자에게 중요한 접근성 기능입니다. React 충돌 가능성만으로 전체 페이지의 번역을 막는 것은 과한 대응일 수 있습니다.
그런 사이트라면 먼저 실제 콘솔 예외를 수집하고, 자주 바뀌는 컴포넌트와 번역 대상 텍스트를 분리하거나 문제가 나는 영역만 translate="no"로 제외하는 편이 낫습니다. 오류 경계도 페이지 전체가 아니라 작은 기능 단위로 나누면 한 컴포넌트의 충돌이 전체 화면을 덮는 일을 줄일 수 있습니다.
또한 이 속성은 브라우저와 번역 도구에 전달하는 힌트이지 보안 장치가 아닙니다. 일부 확장 프로그램이나 사용자가 강제로 실행한 도구는 무시할 수 있습니다.
정리
번역 후 사이트의 오류 화면이 보인다면 먼저 그 문구가 브라우저 것인지 애플리케이션 오류 경계 것인지 확인합니다. React 오류라면 번역 스크립트가 DOM을 바꾼 뒤 동적 렌더링과 충돌했을 가능성을 봅니다.
suppressHydrationWarning은 이 문제를 해결하지 않습니다. DOM 메서드를 전역 패치하는 방식도 피하는 편이 안전합니다. 이미 언어별 페이지를 제공하는 사이트라면 notranslate와 translate="no"로 브라우저 번역 계층 자체를 제외하는 것이 가장 단순하고 예측 가능한 해결책입니다.
정확한 예외 이름과 호출 위치까지 확정하려면 배포 환경의 브라우저 콘솔이나 오류 수집 도구에서 첫 번째 스택을 확인해야 합니다. 이번 사례에서는 자체 오류 경계 진입 시점, Google 번역의 DOM 변경 방식, React에 보고된 동일 계열의 충돌을 함께 근거로 원인을 좁혔습니다.
참고 자료
- Chromium Translate Design Document
- React issue #11538: Google Translate와 DOM 조작 충돌
- HTML Living Standard:
translate속성
같이 읽기:
함께 읽기
- Next.js는 운영에서 멀쩡한데 로컬 개발 서버에서만 UI가 먹통이 되는 이유Next.js 애플리케이션은 운영 빌드가 안정적이면 개발 서버도 같은 방식으로 움직일 것 같지만, 둘은 클라이언트 코드의 수명이 다릅니다. 제가 확인한 환경은 Next.js 16.2.7과 Turbopack이었고, 운영 사이트에서는 잘 눌리던 상단 메뉴가 로컬 개발 서버에서만 간헐적으로 먹통이 됐습니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…
- Next.js에서 링크를 눌렀는데 로딩 뒤 이전 화면이 다시 나타나는 이유페이지 이동을 빠르게 보이게 만드는 장치는 대개 눈에 잘 띄지 않습니다. 미리 다음 화면을 받아 두거나, 서버 응답을 기다리는 동안 스켈레톤을 보여주는 식입니다. 이번에는 그 두 가지가 한꺼번에 작동하면서 오히려 이동을 늦추고 화면 상태까지 헷갈리게 만들었습니다.
- 스크롤바가 없는데 화면 오른쪽에 빈 여백이 생기는 이유웹 앱의 화면 가장자리는 평소에는 눈에 잘 들어오지 않습니다. 콘텐츠와 배경이 자연스럽게 이어지면 굳이 볼 이유가 없기 때문입니다. 그런데 여러 화면을 확인하던 중 오른쪽 끝에 비어 보이는 세로 띠가 계속 눈에 걸렸습니다. 어떤 화면에서는 나타나고, 어떤 화면에서는 덜 보였습니다.
- 디자인 시안을 웹으로 옮기는 순서: 시맨틱 HTML·토큰·컴포넌트디자인 시안을 웹으로 옮길 때 핵심은 픽셀을 그대로 복사하는 것이 아니라, 콘텐츠의 의미와 반복 규칙을 브라우저가 이해할 수 있는 구조로 번역하는 것이다. 이 글에서는 시맨틱 HTML, 디자인 토큰, 컴포넌트, 반응형 규칙을 이용해 시안을 유지보수 가능한 코드로 구현하는 순서를 살펴본다.