July 11, 2025

๐Ÿ”‘ ๋ฐฐ์—ด ์ธ๋ฑ์Šค๋ฅผ React ๋ฆฌ์ŠคํŠธ `key`๋กœ ์‚ฌ์šฉํ•˜๊ธฐ: ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ UI์™€ ์„ฑ๋Šฅ ์ €ํ•˜

React
์ปดํฌ๋„ŒํŠธ
์„ฑ๋Šฅ
์—๋Ÿฌ์ฒ˜๋ฆฌ

Summary

React ๋ฆฌ์ŠคํŠธ์—์„œ key prop์œผ๋กœ ๋ฐฐ์—ด index๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์˜ ์ˆœ์„œ ๋ณ€๊ฒฝ, ์ถ”๊ฐ€, ์‚ญ์ œ ์‹œ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ UI ๋ฒ„๊ทธ์™€ ์„ฑ๋Šฅ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค. React๊ฐ€ ํ•ญ๋ชฉ์˜ ์‹ ์›์„ ์ •ํ™•ํžˆ ์ถ”์ ํ•˜๋„๋ก ๊ฐ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์— ๊ณ ์œ ํ•˜๊ณ  ์•ˆ์ •์ ์ธ ์‹๋ณ„์ž(ID)๋ฅผ key๋กœ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Why Wrong?

React๋Š” ๊ฐ€์ƒ DOM์„ ํšจ์œจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด '์žฌ์กฐ์ •(Reconciliation)' ๊ณผ์ •์„ ๊ฑฐ์นฉ๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋ฆฌ์ŠคํŠธ์˜ ๊ฐ ํ•ญ๋ชฉ์„ ์‹๋ณ„ํ•˜๊ณ , ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ถ”์ ํ•˜๋Š” ๋ฐ key prop์ด ํ•ต์‹ฌ์ ์ธ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. key๋Š” ๋ฆฌ์ŠคํŠธ ๋‚ด์—์„œ ๊ฐ ํ•ญ๋ชฉ์˜ ๊ณ ์œ ํ•œ ์‹ ๋ถ„์„ ๋ถ€์—ฌํ•˜๋ฉฐ, React๊ฐ€ ์–ด๋–ค ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋˜์—ˆ๋Š”์ง€, ์ œ๊ฑฐ๋˜์—ˆ๋Š”์ง€, ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€๋ฅผ ํŒŒ์•…ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๋ฐฐ์—ด์˜ index๋ฅผ key๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•ฉ๋‹ˆ๋‹ค:

  1. ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ UI ๋ฒ„๊ทธ ๋ฐ ์ƒํƒœ ์œ ์‹ค: ๋ฆฌ์ŠคํŠธ์˜ ํ•ญ๋ชฉ ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜, ์ค‘๊ฐ„์— ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€/์‚ญ์ œ๋  ๊ฒฝ์šฐ, ๊ธฐ์กด ํ•ญ๋ชฉ๋“ค์˜ index๊ฐ€ ๋ฐ”๋€Œ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. React๋Š” ๋ฐ”๋€ index๋ฅผ ๊ธฐ์ค€์œผ๋กœ DOM ์š”์†Œ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ์ด์ „์— ๋ Œ๋”๋ง๋œ DOM ์š”์†Œ์˜ ๋‚ด๋ถ€ ์ƒํƒœ(์˜ˆ: <input> ํ•„๋“œ์˜ ๊ฐ’, <checkbox>์˜ ์ฒดํฌ ์ƒํƒœ)๊ฐ€ ๋‹ค๋ฅธ ํ•ญ๋ชฉ์— ์ž˜๋ชป ์—ฐ๊ฒฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋‚ด์šฉ์ด ์‚ฌ๋ผ์ง€๊ฑฐ๋‚˜, ์ฒดํฌ๋ฐ•์Šค๊ฐ€ ๋งˆ์Œ๋Œ€๋กœ ์ฒดํฌ/ํ•ด์ œ๋˜๋Š” ๋“ฑ ์น˜๋ช…์ ์ธ UI ๋ฒ„๊ทธ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

  2. ๋น„ํšจ์œจ์ ์ธ ์„ฑ๋Šฅ: key๊ฐ€ ๋ถˆ์•ˆ์ •ํ•˜๋ฉด React๋Š” ํ•ญ๋ชฉ์˜ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ ํ•ด๋‹น ํ•ญ๋ชฉ์„ ์žฌํ™œ์šฉํ•˜๊ธฐ๋ณด๋‹ค ๊ธฐ์กด DOM ์š”์†Œ๋ฅผ ํŒŒ๊ดดํ•˜๊ณ  ์ƒˆ๋กœ์šด DOM ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝํ–ฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ถˆํ•„์š”ํ•œ DOM ์กฐ์ž‘์„ ์œ ๋ฐœํ•˜์—ฌ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ €ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ๋Œ€๊ทœ๋ชจ ๋ฆฌ์ŠคํŠธ์—์„œ ์ด ๋ฌธ์ œ๋Š” ๋”์šฑ ๋‘๋“œ๋Ÿฌ์ง‘๋‹ˆ๋‹ค.

  3. ์ปดํฌ๋„ŒํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด ๋ฌธ์ œ: key๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด React๋Š” ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์™„์ „ํžˆ ์–ธ๋งˆ์šดํŠธํ•˜๊ณ  ๋‹ค์‹œ ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ถˆํ•„์š”ํ•œ useEffect ํด๋ฆฐ์—… ๋ฐ ์žฌ์‹คํ–‰, ๊ทธ๋ฆฌ๊ณ  ์ƒํƒœ ์ดˆ๊ธฐํ™”๋ฅผ ์ดˆ๋ž˜ํ•˜์—ฌ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ถ€์ž‘์šฉ์„ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

How to Fix?

๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง ์‹œ์—๋Š” ๋ฐ˜๋“œ์‹œ ๊ฐ ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์— ๊ณ ์œ ํ•˜๊ณ  ์•ˆ์ •์ ์ธ(unique and stable) ์‹๋ณ„์ž๋ฅผ key prop์œผ๋กœ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ API์—์„œ ์˜ค๋Š” ๊ฒฝ์šฐ, ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๊ณ ์œ ํ•œ id ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด id๋ฅผ key๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  • ๊ณ ์œ  ID ์‚ฌ์šฉ: ๋ฐ์ดํ„ฐ๊ฐ€ ์ด๋ฏธ ๊ณ ์œ ํ•œ ID๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด ํ•ด๋‹น ID๋ฅผ key๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • UUID ์ƒ์„ฑ: ๋งŒ์•ฝ ๋ฐ์ดํ„ฐ์— ๊ณ ์œ  ID๊ฐ€ ์—†๋‹ค๋ฉด, uuid์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๊ณ ์œ ํ•œ ID๋ฅผ ์ƒ์„ฑํ•˜์—ฌ key๋กœ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ, ์ด ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅ๋  ๋•Œ๋„ ์ด ID๊ฐ€ ์œ ์ง€๋˜๋„๋ก ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ตœํ›„์˜ ์ˆ˜๋‹จ (๋งค์šฐ ์ œํ•œ์ ): ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ ˆ๋Œ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์œผ๋ฉฐ(ํ•ญ๋ชฉ ์ถ”๊ฐ€/์‚ญ์ œ/์žฌ์ •๋ ฌ ์—†์Œ) ๊ฐ ํ•ญ๋ชฉ์ด ๋ณธ์งˆ์ ์œผ๋กœ ๊ณ ์œ ํ•œ ๊ฒฝ์šฐ์—๋งŒ index๋ฅผ key๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์€ ์‹ค๋ฌด์—์„œ ๋งค์šฐ ๋“œ๋ฌผ๋ฉฐ, ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๋Š” ๋™์ ์œผ๋กœ ๋ณ€ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฏ€๋กœ ์‹ ์ค‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Before Code (Bad)

import React, { useState } from 'react';

function BadKeyExample() {
  const [todos, setTodos] = useState([
    { id: 1, text: '์•„์นจ ์‹์‚ฌ' },
    { id: 2, text: '์ฝ”๋“œ ๋ฆฌ๋ทฐ' },
    { id: 3, text: '์ ์‹ฌ ์‹์‚ฌ' },
  ]);

  const removeTodo = (indexToRemove) => {
    setTodos(todos.filter((_, index) => index !== indexToRemove));
  };

  const addTodo = () => {
    const newTodo = { id: Date.now(), text: `์ƒˆ๋กœ์šด ํ•  ์ผ ${todos.length + 1}` };
    setTodos([newTodo, ...todos]); // ๋งจ ์•ž์— ์ƒˆ ํ•  ์ผ ์ถ”๊ฐ€
  };

  return (
    <div>
      <h3>โŒ ๋ฐฐ์—ด ์ธ๋ฑ์Šค๋ฅผ key๋กœ ์‚ฌ์šฉ</h3>
      <button onClick={addTodo}>๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€</button>
      <ul style={{ border: '1px solid red', padding: '10px' }}>
        {todos.map((todo, index) => (
          <li key={index} style={{ marginBottom: '5px' }}> {/* โš ๏ธ ๋ฌธ์ œ์˜ ์›์ธ: index๋ฅผ key๋กœ ์‚ฌ์šฉ */}
            <input type="text" value={todo.text} onChange={(e) => {
              const newTodos = [...todos];
              newTodos[index].text = e.target.value;
              setTodos(newTodos);
            }} />
            <button onClick={() => removeTodo(index)} style={{ marginLeft: '10px' }}>์‚ญ์ œ</button>
          </li>
        ))}
      </ul>
      <p style={{ color: 'red', fontWeight: 'bold' }}>
        โš ๏ธ '๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€' ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ํ›„, ๊ธฐ์กด '์•„์นจ ์‹์‚ฌ' ์นธ์— 'ํ…Œ์ŠคํŠธ'๋ผ๊ณ  ์ž…๋ ฅํ•ด๋ณด์„ธ์š”. ๊ทธ๋ฆฌ๊ณ  ๋‹ค์‹œ '๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€'๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ ์ค‘๊ฐ„์˜ '์ฝ”๋“œ ๋ฆฌ๋ทฐ'๋ฅผ ์‚ญ์ œํ•˜๋ฉด ์ž…๋ ฅ๊ฐ’์ด ์—‰ํ‚ค๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      </p>
    </div>
  );
}

export default BadKeyExample;

After Code (Good)

import React, { useState } from 'react';

function GoodKeyExample() {
  const [todos, setTodos] = useState([
    { id: 1, text: '์•„์นจ ์‹์‚ฌ' },
    { id: 2, text: '์ฝ”๋“œ ๋ฆฌ๋ทฐ' },
    { id: 3, text: '์ ์‹ฌ ์‹์‚ฌ' },
  ]);

  const removeTodo = (idToRemove) => {
    setTodos(todos.filter(todo => todo.id !== idToRemove));
  };

  const addTodo = () => {
    const newTodo = { id: Date.now(), text: `์ƒˆ๋กœ์šด ํ•  ์ผ ${todos.length + 1}` }; // ๊ณ ์œ  ID ์ƒ์„ฑ
    setTodos([newTodo, ...todos]); // ๋งจ ์•ž์— ์ƒˆ ํ•  ์ผ ์ถ”๊ฐ€
  };

  return (
    <div>
      <h3>โœ… ๊ณ ์œ ํ•œ ID๋ฅผ key๋กœ ์‚ฌ์šฉ</h3>
      <button onClick={addTodo}>๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€</button>
      <ul style={{ border: '1px solid green', padding: '10px' }}>
        {todos.map((todo) => (
          <li key={todo.id} style={{ marginBottom: '5px' }}> {/* โœจ ํ•ด๊ฒฐ: ๊ณ ์œ ํ•œ todo.id๋ฅผ key๋กœ ์‚ฌ์šฉ */}
            <input type="text" value={todo.text} onChange={(e) => {
              const newTodos = todos.map(item => 
                item.id === todo.id ? { ...item, text: e.target.value } : item
              );
              setTodos(newTodos);
            }} />
            <button onClick={() => removeTodo(todo.id)} style={{ marginLeft: '10px' }}>์‚ญ์ œ</button>
          </li>
        ))}
      </ul>
      <p style={{ color: 'green', fontWeight: 'bold' }}>
        โœจ '๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€' ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ํ›„, ๊ธฐ์กด '์•„์นจ ์‹์‚ฌ' ์นธ์— 'ํ…Œ์ŠคํŠธ'๋ผ๊ณ  ์ž…๋ ฅํ•ด๋ณด์„ธ์š”. ๋‹ค์‹œ '๋งจ ์•ž์— ํ•  ์ผ ์ถ”๊ฐ€'๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ ์ค‘๊ฐ„์˜ '์ฝ”๋“œ ๋ฆฌ๋ทฐ'๋ฅผ ์‚ญ์ œํ•ด๋„ ์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.
      </p>
    </div>
  );
}

export default GoodKeyExample;