useId는 컴포넌트 인스턴스마다 고유한 id를 반환하며, 렌더링마다 같은 id를 반환합니다. 컴포넌트가 한 페이지에 여러 번 나타날 수 있을 때, <label>을 <input>에 연결하거나 aria-describedby로 input을 힌트에 연결하는 데 사용하세요.
같은 컴포넌트가 두 번 렌더링되고 서로 다른 두 id를 받으며, 각 필드 아래에 출력됩니다. "Confirm password" 텍스트를 클릭하면 그 라벨의 htmlFor가 그 input의 id와만 일치하므로 포커스가 두 번째 input으로 이동합니다. useId() 대신 id="password"를 직접 쓰면 두 라벨 모두 첫 번째 input을 가리킵니다.
문법
const id = useId();
useId는 인자를 받지 않고 문자열을 반환합니다. 다른 모든 훅처럼 컴포넌트의 최상위에서 호출하세요. 정확한 형식은 내부 사항이며 버전마다 바뀌어 왔습니다. React 18은 :r1:을 만들었고, React 19.2는 브라우저에서 처음 렌더링된 컴포넌트에는 _r_1_을, 서버에서 렌더링된 컴포넌트에는 트리 위치로 만든 _R_로 시작하는 id를 만듭니다. 절대 파싱하거나 그 모양에 의존하지 마세요.
Math.random이나 카운터를 쓰면 안 되는 이유
접근성을 위한 id는 서버가 보내는 HTML과 React가 브라우저에서 만드는 트리 사이에서 일치해야 합니다. id를 만드는 뻔한 두 방법은 모두 이 조건을 통과하지 못합니다.
// Changes on every render, and differs between server and browser
const id = 'field-' + Math.random().toString(36).slice(2);
// The server's counter keeps growing across requests, the browser starts at 0
let nextId = 0;
const id = 'field-' + nextId++;
서버 렌더링(Next.js, React Router 프레임워크 모드, 모든 hydrateRoot 구성)에서는 서버가 HTML에 id="field-4817"을 출력하고, 브라우저의 첫 렌더링은 field-0을 계산하며, React는 하이드레이션 불일치를 보고합니다. useId는 트리 안의 컴포넌트 위치에서 id를 만들며, 이 위치는 양쪽에서 똑같습니다.
서버가 없어도 렌더링 중에 만든 id는 렌더링마다 바뀔 수 있습니다. 이 예제는 서버 없이 그 차이를 보여 줍니다.
버튼을 몇 번 클릭해 보세요. useId 값은 그대로인데 카운터 id는 렌더링마다 올라가므로, 이전 id를 가리키던 것(aria-describedby, 라벨)은 이제 아무것도 가리키지 않습니다. 카운터를 useState(() => nextId++)로 감싸면 리렌더링 문제는 해결되지만 서버 불일치는 해결되지 않습니다.
호출 하나로 여러 id 만들기
필드가 여러 개인 컴포넌트에 useId 호출이 여러 개 필요한 것은 아닙니다. 기본 id 하나를 만들고 요소마다 접미사를 붙이세요.
email 필드에 @가 없는 단어를 입력해 보세요. 오류가 나타나고 input의 aria-describedby가 그것을 가리키므로, input에 포커스가 가면 스크린 리더가 오류를 읽어 줍니다. App에서 <SignupForm />을 두 번 렌더링하면 각 사본이 자기만의 기본 id를 받습니다.
목록 key로는 쓰지 마세요
key와 id는 서로 다른 문제를 해결합니다. key는 렌더링 사이에 어느 항목이 어느 항목인지 React에 알려 주므로 데이터에서 와야 합니다. useId는 컴포넌트 인스턴스마다 id 하나를 주며, map 안에서 호출할 수 없습니다.
// Wrong: breaks the rules of hooks, and the key is unrelated to the item
{todos.map((todo) => <Todo key={useId()} todo={todo} />)}
// Right: the key comes from the data
{todos.map((todo) => <Todo key={todo.id} todo={todo} />)}
데이터에 id가 없다면, 렌더링 중이 아니라 항목을 만들 때(항목을 추가하는 이벤트 핸들러에서 crypto.randomUUID()) 만드세요. 목록과 key 페이지에서 key가 왜 항목과 함께 있어야 하는지 설명합니다.
한 페이지의 여러 React 루트
별개의 React 앱 두 개가 같은 페이지에서 렌더링되면 id가 충돌할 수 있습니다. 각 루트에 접두사를 주세요.
createRoot(document.getElementById('cart'), { identifierPrefix: 'cart-' });
createRoot(document.getElementById('chat'), { identifierPrefix: 'chat-' });
서버 렌더링에서는 같은 identifierPrefix를 서버 렌더러와 hydrateRoot에 모두 넘겨서 양쪽이 같은 id를 만들게 하세요.
자주 하는 실수
서버와 브라우저에서 다른 트리 렌더링하기. useId는 컴포넌트의 위치에 의존하므로, 필드 위에 typeof window === 'undefined' ? <A /> : <B /> 같은 분기가 있으면 두 렌더링 사이에 id가 밀릴 수 있습니다. 하이드레이션 동안에는 트리를 같게 유지하고, 그 뒤에 이펙트에서 바꾸세요.
id로 요소 찾기. document.getElementById(id)도 동작하지만, DOM 노드에 접근하는 React 방식은 ref이며 id가 전혀 필요 없습니다.
무작위 값으로 쓰기. 이 id는 앱 안에서 고유할 뿐 무작위가 아니며, 트리로부터 예측할 수 있습니다. 보안 토큰, 캐시 키, 세션을 넘어 저장하는 어떤 것에도 쓰지 마세요.
언제 쓸까
재사용 가능한 컴포넌트에 id 속성이 필요할 때마다 useId를 사용하세요. 여러 번 쓰이는 컴포넌트로 만든 폼 필드, aria-describedby로 연결한 툴팁, aria-labelledby를 가진 대화상자, aria-controls를 가진 탭이 그렇습니다. id가 라벨과 input을 연결하기만 한다면, id를 생략하고 input을 라벨 안에 중첩해도 됩니다(<label>Name <input /></label>). 요소를 중첩할 수 없을 때 useId를 사용하세요.
자주 묻는 질문
React에서 useId는 무엇을 하나요?
그 컴포넌트 인스턴스에 고유하고 렌더링마다 같은 문자열을 반환합니다. 라벨의 htmlFor, input의 aria-describedby, 대화상자의 aria-labelledby처럼 id로 요소를 연결할 때 사용합니다.
id에 Math.random()이나 카운터를 쓰면 안 되는 이유는 무엇인가요?
서버와 브라우저에서 다른 값을 만들기 때문에, 서버에서 렌더링한 페이지와 하이드레이션된 버전이 어긋나고 React가 하이드레이션 불일치를 보고합니다. Math.random()은 렌더링마다 바뀌기도 합니다. useId는 트리 안의 컴포넌트 위치에서 id를 만들며, 이 위치는 양쪽에서 같습니다.
목록의 key로 useId를 써도 되나요?
아니요. React가 렌더링 사이에 같은 항목을 짝지을 수 있도록 key는 데이터에서 와야 합니다. useId는 컴포넌트마다 한 번 호출되며, map 안에서 호출하면 어차피 훅의 규칙을 어기게 됩니다. 항목 자체의 id를 사용하세요.
useId 호출 하나로 여러 id를 얻으려면 어떻게 하나요?
useId를 한 번 호출하고 접미사를 붙이세요: ${id}-name, ${id}-email. 기본 id가 고유하므로 접미사를 붙인 문자열도 고유합니다.
useId의 id를 CSS 선택자에 써도 되나요?
피하세요. 이 id는 DOM에서 요소를 연결하기 위한 것이고, 정확한 형식은 React 버전에 따라 바뀌어 온 내부 세부 사항입니다. 스타일은 클래스로 주고, 요소는 querySelector 대신 ref로 찾으세요.