useId๋Š” ์ ‘๊ทผ์„ฑ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ณ ์œ  ID๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ React Hook์ž…๋‹ˆ๋‹ค.

const id = useId()

๋ ˆํผ๋Ÿฐ์Šค

useId()

useId๋ฅผ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„์—์„œ ํ˜ธ์ถœํ•˜์—ฌ ๊ณ ์œ  ID๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

import { useId } from 'react';

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

์•„๋ž˜์—์„œ ๋” ๋งŽ์€ ์˜ˆ์‹œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๋งค๊ฐœ๋ณ€์ˆ˜

useId๋Š” ์–ด๋–ค ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜๊ฐ’

useId๋ฅผ ํ˜ธ์ถœํ•œ ํŠน์ • ์ปดํฌ๋„ŒํŠธ์™€ ํŠน์ • useId์— ๊ด€๋ จ๋œ ๊ณ ์œ  ID ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ ์‚ฌํ•ญ

  • useId๋Š” Hook์ด๋ฏ€๋กœ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋˜๋Š” ์ปค์Šคํ…€ Hook์—์„œ๋งŒ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ณต๋ฌธ์ด๋‚˜ ์กฐ๊ฑด๋ฌธ์—์„œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”์ถœํ•˜๊ณ  ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋กœ state๋ฅผ ์ด๋™ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • useId๋ฅผ ๋ฆฌ์ŠคํŠธ์˜ key๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. Key๋Š” ๋ฐ์ดํ„ฐ๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์‚ฌ์šฉ๋ฒ•

์ฃผ์˜ํ•˜์„ธ์š”!

useId๋ฅผ ๋ฆฌ์ŠคํŠธ์˜ key๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. Key๋Š” ๋ฐ์ดํ„ฐ๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ ‘๊ทผ์„ฑ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ์œ„ํ•œ ๊ณ ์œ  ID ์ƒ์„ฑํ•˜๊ธฐ

๊ณ ์œ  ID๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด useId๋ฅผ ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ๋‹จ์—์„œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

import { useId } from 'react';

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

์ƒ์„ฑ๋œ ID๋ฅผ ๋‹ค๋ฅธ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ์œ ์šฉํ•œ ์ƒํ™ฉ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

aria-describedby์™€ ๊ฐ™์€ HTML ์ ‘๊ทผ์„ฑ ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‘ ๊ฐœ์˜ ํƒœ๊ทธ๊ฐ€ ์„œ๋กœ ์—ฐ๊ด€๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์—˜๋ฆฌ๋จผํŠธ(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>

React์—์„œ ID๋ฅผ ์ง์ ‘ ์ฝ”๋“œ์— ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ์‚ฌ๋ก€๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ํŽ˜์ด์ง€์—์„œ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ช‡ ๋ฒˆ์ด๊ณ  ๋ Œ๋”๋ง ๋  ์ˆ˜ ์žˆ์ง€๋งŒ 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๋Š” ์ถฉ๋Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

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>
    </>
  );
}

export default function App() {
  return (
    <>
      <h2>Choose password</h2>
      <PasswordField />
      <h2>Confirm password</h2>
      <PasswordField />
    </>
  );
}

์˜์ƒ์„ ํ†ตํ•ด ๋ณด์กฐ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์˜ ์ฐจ์ด์ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์˜ํ•˜์„ธ์š”!

์„œ๋ฒ„ ๋ Œ๋”๋ง์—์„œ useId๋Š” ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ผํ•œ ์ปดํฌ๋„ŒํŠธ ํŠธ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋งํ•˜๋Š” ํŠธ๋ฆฌ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์ƒ์„ฑ๋œ ID๋Š” ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Deep Dive

useId๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์นด์šดํ„ฐ๋ฅผ ์ฆ๊ฐ€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚˜์€ ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”?

useId๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด nextId++์ฒ˜๋Ÿผ ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์ฆ๊ฐ€ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚˜์€ ์ด์œ ์— ๋Œ€ํ•ด ๊ถ๊ธˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

useId์˜ ์ฃผ์š” ์ด์ ์€ React๊ฐ€ ์„œ๋ฒ„ ๋ Œ๋”๋ง๊ณผ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋„๋ก ๋ณด์žฅํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๋ Œ๋”๋ง์„ ํ•˜๋Š” ๋™์•ˆ ์ปดํฌ๋„ŒํŠธ๋Š” HTML ๊ฒฐ๊ณผ๋ฌผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„, ํด๋ผ์ด์–ธํŠธ์—์„œ hydration์ด HTML ๊ฒฐ๊ณผ๋ฌผ์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. hydration์ด ๋™์ž‘ํ•˜๋ ค๋ฉด ํด๋ผ์ด์–ธํŠธ์˜ ์ถœ๋ ฅ์ด ์„œ๋ฒ„ HTML๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ hydrated ์ˆœ์„œ๊ฐ€ ์„œ๋ฒ„ 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>
  );
}

useId๋ฅผ ๊ณ ์œ ํ•œ ID๊ฐ€ ํ•„์š”ํ•œ ๋ชจ๋“  ์—˜๋ฆฌ๋จผํŠธ์—์„œ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ƒ์„ฑ๋œ ๋ชจ๋“  ID์— ๋Œ€ํ•ด ๊ณต์œ  ์ ‘๋‘์‚ฌ ์ง€์ •ํ•˜๊ธฐ

์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…๋ฆฝ๋œ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€์—์„œ ๋ Œ๋”๋งํ•œ๋‹ค๋ฉด identifierPrefix๋ฅผ createRoot ๋˜๋Š” hydrateRoot ํ˜ธ์ถœ์— ๋Œ€ํ•œ ์˜ต์…˜์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. useId๋กœ ์ƒ์„ฑ๋œ ๋ชจ๋“  ์‹๋ณ„์ž๊ฐ€ ๋ณ„๊ฐœ์˜ ์ ‘๋‘์‚ฌ๋กœ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ ์„œ๋กœ ๋‹ค๋ฅธ ๋‘ ๊ฐœ์˜ ์•ฑ์—์„œ ์ƒ์„ฑ๋œ ID๊ฐ€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

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 />);