useId

Chaerin Kim·2023년 12월 12일

접근성 attribute에 전달할 수 있는 고유 ID를 생성하기 위한 React Hook

const id = useId()

Reference

useId()

컴포넌트의 최상위 수준에서 useId를 호출하여 고유 ID를 생성할 수 있음:

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  
  // ...

Parameters

parameters를 받지 않음.

Returns

이 특정 컴포넌트에서 이 특정 useId 호출과 관련된 고유 ID 문자열을 반환함.

Caveats

  • useId는 Hook이므로 컴포넌트의 최상위 수준이나 자체 Hook에서만 호출할 수 있음. 루프나 조건 내부에서는 호출할 수 없음. 필요한 경우 새 컴포넌트를 추출하고 state를 그 안으로 옮길 것.

  • useId는 목록에서 key를 생성하는 데 사용해서는 안됨. 키는 데이터로부터 생성해야 함.


Usage

Pitfall

목록에서 key를 생성하기 위해 useId를 호출하지 말 것! 키는 데이터로부터 생성해야 함.

Generating unique IDs for accessibility attributes

컴포넌트의 최상위 수준에서 useId를 호출하여 고유 ID를 생성:

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  
  // ...

그런 다음 생성된 ID를 다른 attribute에 전달할 수 있음:

<>
  <input type="password" aria-describedby={passwordHintId} />
  <p id={passwordHintId}>
</>

이 기능은 언제 유용한가?

aria-describedby와 같은 HTML 접근성 attribute를 사용하면 두 태그가 서로 관련되어 있음을 지정할 수 있음. 예를 들어, input과 같은 요소가 paragraph와 같은 다른 요소에 의해 설명되도록 지정할 수 있음.

일반 HTML에서는 다음과 같이 작성함:

<label>
  Password:
  <input
    type="password"
    aria-describedby="password-hint"
  />
</label>
<p id="password-hint">
  The password should contain at least 18 characters
</p>

그러나 이와 같이 ID를 하드코딩하는 것은 React에서 좋은 방법이 아님. 컴포넌트는 페이지에서 두 번 이상 렌더링될 수 있지만 ID는 고유해야 함! ID를 하드코딩하는 대신 useId로 고유한 ID를 생성할 것:

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  return (
    <>
      <label>
        Password:
        <input
          type="password"
          aria-describedby={passwordHintId}
        />
      </label>
      <p id={passwordHintId}>
        The password should contain at least 18 characters
      </p>
    </>
  );
}

이제 PasswordField가 화면에 여러 번 표시되더라도 생성된 ID가 충돌하지 않음.

Pitfall

서버 렌더링의 경우 useId를 사용하려면 서버와 클라이언트에서 동일한 컴포넌트 트리가 필요ㅎ함. 서버와 클라이언트에서 렌더링하는 트리가 정확히 일치하지 않으면 생성된 ID가 일치하지 않음.

DEEP DIVE: Why is useId better than an incrementing counter?

useIdnextId++와 같이 전역 변수를 증가시키는 것보다 나은 이유는?

useId의 가장 큰 장점은 React가 서버 렌더링에서도 작동하도록 보장한다는 것. 서버 렌더링 중에 컴포넌트는 HTML 출력을 생성함. 나중에 클라이언트에서 hydration을 통해 생성된 HTML에 이벤트 핸들러를 첨부함. Hydration 작동하려면 클라이언트 출력이 서버 HTML과 일치해야 함.

클라이언트 컴포넌트가 hydration되는 순서가 서버 HTML이 출력되는 순서와 일치하지 않을 수 있기 때문에 증분 카운터로는 이를 보장하기가 매우 어려움. useId를 호출하면 hydration이 작동하고 서버와 클라이언트 간에 출력이 일치하는지 확인할 수 있음.

React 내부에서 useId는 호출하는 컴포넌트의 "부모 경로"에서 생성됨. 그렇기 때문에 클라이언트와 서버 트리가 동일하면 렌더링 순서에 관계없이 "부모 경로"가 일치함.

관련된 여러 요소에 ID를 부여해야 하는 경우 useId를 호출하여 해당 요소들의 공통 접두사를 생성할 수 있음:

import { useId } from 'react';

export default function Form() {
  const id = useId();
  return (
    <form>
      <label htmlFor={id + '-firstName'}>First Name:</label>
      <input id={id + '-firstName'} type="text" />
      <hr />
      <label htmlFor={id + '-lastName'}>Last Name:</label>
      <input id={id + '-lastName'} type="text" />
    </form>
  );
}

이렇게 하면 고유 ID가 필요한 모든 요소에 대해 useId를 호출하지 않아도 됨.

Specifying a shared prefix for all generated IDs

단일 페이지에서 여러 개의 독립적인 React 애플리케이션을 렌더링하는 경우, createRoot 또는 hydrateRoot 호출에 identifierPrefix를 옵션으로 전달할 것. 이렇게 하면 useId로 생성된 모든 식별자가 지정한 고유한 접두사로 시작하므로 서로 다른 두 앱에서 생성된 ID가 충돌하지 않음.

// index.js
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

const root1 = createRoot(document.getElementById('root1'), {
  identifierPrefix: 'my-first-app-'
});
root1.render(<App />);

const root2 = createRoot(document.getElementById('root2'), {
  identifierPrefix: 'my-second-app-'
});
root2.render(<App />);

Using the same ID prefix on the client and the server

동일한 페이지에 여러 개의 독립적인 React 앱을 렌더링하고 이러한 앱 중 일부가 서버에서 렌더링되는 경우, 클라이언트 측에서 hydrateRoot 호출에 전달하는 identifierPrefixrenderToPipeableStream과 같은 서버 API에 전달하는 identifierPrefix와 동일한지 확인할 것.

// Server
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(
  <App />,
  { identifierPrefix: 'react-app1' }
);
// Client
import { hydrateRoot } from 'react-dom/client';

const domNode = document.getElementById('root');
const root = hydrateRoot(
  domNode,
  reactNode,
  { identifierPrefix: 'react-app1' }
);

페이지에 React 앱이 하나만 있는 경우 identifierPrefix를 전달할 필요가 없음.

0개의 댓글