React Hook - useId

김소희·2025년 11월 30일

js로 고유한 아이디를 만드는 방법

  • JS에서 제공하는 Math.random()
  • uuid와 같은 라이브러리 사용
  • 리액트의 useId()

useId란

useId()는 리액트 18버전에 추가된 훅으로, 접근성을 고려한 폼 요소 연결과 고유 식별자 생성을 위해 설계되었다. 이 훅은 컴포넌트의 트리 구조 내 위치를 기반으로 결정적인 ID를 생성하므로, 같은 컴포넌트는 마운트와 리렌더링에 관계없이 항상 동일한 ID를 유지한다.

useId의 장점

  • useId로 만든 id값은 생김새가 항상 :을 포함하고 있다. 우리가 특정 돔 요소에 접근할 때 쿼리셀렉터를 사용하곤 하는데, CSS 선택자 문법에서 :은 가상 클래스를 의미하므로 이스케이프 처리 없이는 제대로 선택할 수 없다. 예를 들어 document.querySelector('#:r1:')같은 코드는 구문 오류를 발생시킨다. 이것이 왜 장점이 되냐면, 리액트는 이미 돔 요소에 접근하는 잘 설계된 기능인 ref를 제공하고 있는데, 굳이 쿼리셀렉터 같은 좋지 않은 방법을 사용할 필요가 없기 때문에 막아주는 것이 장점이 된다.

  • 만약 useId가 아닌 Math.random()으로 id값을 만든다면 컴포넌트가 렌더링될 때마다 id값이 새로운 값으로 변경되게 된다. 컴포넌트 함수가 호출될 때마다 내부의 값이 초기화되기 때문이다. id가 바뀔 때마다 폼 요소의 id값도 계속 바뀌기 때문에 포커스 관리나 aria-describedby 같은 접근성 속성들의 참조가 깨져서 스크린 리더를 사용하는 사용자에게 문제가 된다. 반면 useId는 렌더링과 상관없이 id가 유지되므로 이런 문제를 방지할 수 있다.

  • 서버 사이드 렌더링을 사용하는 프로젝트에서 안정성이 증가한다. 서버에서 페이지를 렌더링한 후에 클라이언트로 전달을 할 때, 클라이언트는 받아온 페이지를 하이드레이션이라는 과정을 통해서 상호 작용이 가능한 페이지로 렌더링한다. Math.random()이나 Date.now() 같은 비결정적 방식으로 id를 생성하면 서버와 클라이언트가 각각 다른 시점에 렌더링하므로 항상 다른 id가 생성된다. 이로 인해 서버에서 렌더링된 결과물과 클라이언트가 렌더링한 결과물이 일치하지 않는 하이드레이션 불일치 문제가 발생한다. useId는 리액트의 컴포넌트 트리 구조를 기반으로 결정적인 id를 생성하므로, 같은 컴포넌트 위치에서는 서버와 클라이언트가 동일한 id를 생성하게 되어 이런 문제를 방지할 수 있다.

useId의 사용방법

사용 방법은 정말 간단하다.
리액트를 import해서 사용하면 된다. 인자를 받지 않기 때문에
바로 호출 가능하며 문자열 형태의 아이디를 반환해 준다.

실행 결과로 생성되는 ID를 콘솔에 찍어보면 :r0:, :r1:, :r2: 같은 형태의 문자열이다. 정확한 값은 리액트 내부 상태에 따라 달라지지만, 일반적으로 콜론으로 구분된 형태로 나타난다. 예를 들어 첫 번째 컴포넌트에서 useId()를 호출하면 :r0:과 같은 값을 얻게 된다.

import { useId } from 'react';

function App() {
  const id = useId();
  
  return (
  	<div>
      <lavel>이름</lavel>
      <input />
    </div>
  )
}

export default App;

이 아이디를 input 같은 폼 요소에 접근성에 관련된 속성을 제공할 때 유용함이 발휘된다.
위 코드는 input 요소와 그것에 무엇이 들어가지는지 명시해주는 이름 레이블이 있다.
그러나 두 요소의 연관성을 코드상으로 명시해주지 않은 개별적인 요소이다.
두 코드의 접근성을 높이기 위해서 연결을 해보자.

import { useId } from 'react';

function App() {
  const id = useId();
  
  return (
  	<div>
      <lavel htmlFor="name">이름</lavel>
      <input id="name"/>
    </div>
  ) 
}

export default App;

이렇게 생성된 ID는 label의 for 속성과 input의 id 속성이 동일한 값으로 연결되어 접근성을 보장한다. 이름 영역을 클릭해도 인풋 박스에 포커스가 자동으로 들어오는 것을 확인 할 수 있다. 또한 웹페이지를 읽어주는 스크린 리더도 사용자가 인풋 박스를 클릭하면 레이블을 읽어준다.

그러나 만약 재사용을 위해 컴포넌트화 하면 문제가 발생된다.
두 인풋의 요소 모두 id가 name이기 때문에 한페이지에 중복된 id가 존재하게 되고,
어떤 레이블이 어떤 인풋에 해당하는지 알 방법이 사라진다.
두번째 MyInput의 이름을 선택하면 첫번째 input 박스에 포커스가 생기는 것을 볼 수 있다.

이제 useId를 이용해서 각각의 인풋마다 고유한 아이디를 부여해서 해결해보자.

import { useId } from 'react';

function App() {
  return (
    <div>
      <MyInput />
      <MyInput />
    </div>
  );
}

function MyInput() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}">이름</label>
      <input id={id} />
    </div>
  );
}

export default App;

이제 컴포넌트마다 고유한 아이디가 생성되고, 각각의 레이블은 알맞은 인풋과 연결이 된다.

그런데 개발을 하다보면 하나의 컴포넌트에 여러개의 인풋을 담아야하는 경우도 있다.
이때는 고유한 id도 더 필요해진다.
그럴 땐 useId를 여러개 사용해도 동작하겠지만,
더 좋은 방법은 백틱을 사용해서 뒤에 고유한 이름도 넣어주면 된다.

import { useId } from 'react';

function App() {
  return (
    <div>
      <MyInput />
    </div>
  );
}

function MyInput() {
  const id = useId();
  return (
    <div>
      <label htmlFor={`${id}-name`}>이름</label>
      <input id={`${id}-name`} />
        <br />
      <label htmlFor={`${id}-age`}">나이</label>
      <input id={`${id}-age`} />
    </div>
  );
}

export default App;

참고자료

별코딩 - 유튜브

profile
개발자 소희의 노트

0개의 댓글