label과 input을 연결하려고 만든 id 때문에 하이드레이션 경고가 난 이유
문제 발생
접근성을 위해 label과 input을 연결하려고 id를 만들었는데, 서버 렌더링을 켜자 콘솔에 하이드레이션 불일치 경고가 떴습니다.
function Field({ label }) {
const id = `field-${Math.random().toString(36).slice(2)}`; // 매번 다르다
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}전역 카운터로 바꿔 봤지만, 컴포넌트가 렌더되는 순서가 서버와 클라이언트에서 다를 수 있어 여전히 어긋났습니다.
원인 분석
하이드레이션은 서버가 만든 HTML에 클라이언트가 이벤트 핸들러를 붙이는 과정입니다. 이게 성립하려면 클라이언트가 만든 출력이 서버 HTML과 같아야 합니다. 난수나 전역 카운터로 만든 id는 그 조건을 만족하지 못합니다.
useId는 컴포넌트의 부모 경로(트리 위치) 에서 id를 만듭니다. 위치는 서버와 클라이언트에서 같으므로, 렌더 순서와 무관하게 양쪽이 같은 값을 냅니다. React 공식 문서가 useId의 주된 이점으로 드는 것이 바로 이 서버 렌더링 호환성입니다.
해결 방안
- 접근성 속성용 id는
useId로 만듭니다.
function Field({ label }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}- 한 컴포넌트에 여러 id가 필요하면 접두사를 붙여 씁니다.
useId를 여러 번 호출하지 않아도 됩니다.
const id = useId();
<input id={`${id}-email`} aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>회사 메일을 입력해 주세요</p>- 리스트의
key로 쓰지 않습니다. 공식 문서가 명시적으로 금지합니다 — 키는 데이터에서 만들어야 합니다.useId는 데이터가 아니라 트리 위치에서 나오므로, 항목이 재정렬되면 키가 항목을 따라가지 않습니다. - 한 페이지에 React 앱이 둘 이상이면
identifierPrefix를 줍니다. 각 루트가 만드는 id가 겹치지 않게 됩니다.
createRoot(el, { identifierPrefix: "admin-" });- CSS 선택자로 쓰지 않습니다. 생성되는 값에는 콜론 같은 문자가 포함될 수 있어 선택자로 쓰기에 적합하지 않습니다. 스타일은 클래스로 붙입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.