icon안동민 개발노트

리스트와 키


 React 애플리케이션에서 데이터 컬렉션을 렌더링하는 것은 매우 흔한 작업입니다.

 이 절에서는 리스트를 효율적으로 렌더링하는 방법과 키(key)의 중요성에 대해 알아보겠습니다.

리스트 렌더링 기본

 React에서 리스트를 렌더링할 때는 주로 JavaScript의 map() 함수를 사용합니다.

function NumberList({ numbers }) {
  const listItems = numbers.map((number) =>
    <li>{number}</li>
  );
  return <ul>{listItems}</ul>;
}
 
const numbers = [1, 2, 3, 4, 5];
<NumberList numbers={numbers} />

 이 방식은 간단하지만, React는 "key" prop이 없다는 경고를 표시할 것입니다.

키(Key)의 중요성

 키는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다.

 키는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.

function NumberList({ numbers }) {
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return <ul>{listItems}</ul>;
}

키가 필요한 이유

  1.  성능 최적화 : React는 키를 사용하여 어떤 리스트 항목이 변경되었는지 식별합니다. 이를 통해 전체 리스트를 다시 렌더링하지 않고 변경된 항목만 업데이트할 수 있습니다.

  2.  컴포넌트 상태 유지 : 키는 컴포넌트의 고유성을 보장하므로, 리스트 항목의 순서가 변경되더라도 해당 항목의 상태를 올바르게 유지할 수 있습니다.

  3.  리컨실레이션 프로세스 지원 : React의 재조정(Reconciliation) 과정에서 키를 사용하여 기존 트리와 새로운 트리의 차이를 효율적으로 계산합니다.

키 선택 시 주의사항

  1.  고유성 : 키는 형제 사이에서만 고유하면 됩니다. 전체 애플리케이션 또는 단일 컴포넌트 내에서 고유할 필요는 없습니다.

  2.  안정성 : 키는 변경되지 않아야 합니다. 렌더링할 때마다 새로운 키를 생성하는 것은 좋지 않습니다.

  3.  예측 가능성 : 배열의 인덱스를 키로 사용하는 것은 항목의 순서가 바뀌면 문제가 될 수 있으므로 권장되지 않습니다.

// 좋지 않은 예: 인덱스를 키로 사용
{items.map((item, index) => (
  <li key={index}>
    {item.text}
  </li>
))}
 
// 좋은 예: 고유한 ID를 키로 사용
{items.map((item) => (
  <li key={item.id}>
    {item.text}
  </li>
))}

키를 사용하지 않았을 때의 문제점

 키를 사용하지 않거나 잘못 사용하면 다음과 같은 문제가 발생할 수 있습니다.

  1.  성능 저하 : React가 전체 리스트를 다시 렌더링해야 할 수 있습니다.

  2.  상태 문제 : 리스트 항목의 순서가 변경되면 컴포넌트의 상태가 엉뚱한 항목과 연결될 수 있습니다.

  3.  예상치 못한 동작 : 항목 추가/제거 시 예상치 못한 동작이 발생할 수 있습니다.

 예를 들어, 다음과 같은 TODO 리스트를 고려해봅시다.

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Build an app' }
  ]);
 
  const addTodo = (text) => {
    setTodos([{ id: Date.now(), text }, ...todos]);
  };
 
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>{todo.text}</li> // 인덱스를 키로 사용 (문제 발생 가능)
      ))}
    </ul>
  );
}

 이 예제에서 인덱스를 키로 사용하면, 새 항목을 리스트의 시작 부분에 추가할 때 모든 기존 항목의 키가 변경됩니다.

 이로 인해 React가 모든 항목을 불필요하게 다시 렌더링하게 되며, 특히 각 항목이 자체 상태를 가지고 있다면 문제가 발생할 수 있습니다.

동적으로 변하는 리스트의 효율적인 렌더링

 동적 리스트를 효율적으로 렌더링하기 위해서는 다음 사항들을 고려해야 합니다.

  1.  적절한 키 사용 : 가능하면 항목의 고유 ID를 키로 사용합니다.

  2.  불변성 유지 : 상태를 업데이트할 때 새로운 배열을 생성하여 불변성을 유지합니다.

  3.  최적화 기법 활용 : React.memo, useMemo, useCallback 등을 사용하여 불필요한 리렌더링을 방지합니다.

 다음은 이러한 원칙을 적용한 개선된 TODO 리스트 예제입니다.

import React, { useState, useCallback } from 'react';
 
function TodoItem({ todo, onToggle }) {
  console.log('Rendering:', todo.text);
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.completed ? '✅' : '⬜'} {todo.text}
    </li>
  );
}
 
const MemoizedTodoItem = React.memo(TodoItem);
 
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build an app', completed: false }
  ]);
 
  const addTodo = (text) => {
    setTodos(prevTodos => [
      { id: Date.now(), text, completed: false },
      ...prevTodos
    ]);
  };
 
  const toggleTodo = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);
 
  return (
    <>
      <button onClick={() => addTodo('New Task')}>Add Todo</button>
      <ul>
        {todos.map(todo => (
          <MemoizedTodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
          />
        ))}
      </ul>
    </>
  );
}

 이 예제에서,

  • 각 TODO 항목에 고유한 id를 키로 사용합니다.
  • React.memo를 사용하여 TodoItem 컴포넌트를 메모이제이션합니다.
  • useCallback을 사용하여 toggleTodo 함수를 메모이제이션합니다.
  • 상태 업데이트 시 불변성을 유지합니다.

 이러한 최적화를 통해 리스트의 개별 항목이 변경될 때만 해당 항목이 리렌더링되므로, 큰 리스트에서도 효율적인 성능을 유지할 수 있습니다.

 리스트와 키를 올바르게 사용하는 것은 React 애플리케이션의 성능과 정확성을 보장하는 데 중요합니다.

 적절한 키 선택, 불변성 유지, 그리고 최적화 기법의 적용을 통해 대규모의 동적 리스트도 효율적으로 관리할 수 있습니다.

 이러한 원칙을 따르면 예측 가능하고 성능이 뛰어난 리스트 기반 UI를 구축할 수 있습니다.