Dev Thinking
32완료

반복의 새로운 방식 - 리스트 렌더링과 key의 비밀

2025-08-07
12분 읽기

리액트 공식문서 기반의 리액트 입문기

들어가며

개발을 하다 보면 비슷한 형태의 UI 요소를 반복적으로 화면에 그려야 할 때가 많습니다. 예를 들어, 사용자 목록, 상품 목록, 댓글 목록 등은 모두 데이터 배열을 기반으로 동일한 구조의 아이템들을 나열하는 방식으로 구성됩니다. 바닐라 자바스크립트에서는 주로 for 반복문이나 forEach 메서드를 사용하여 DOM 요소를 직접 생성하고 추가하는 방식으로 이러한 작업을 처리해왔습니다. 각 요소를 만들고, 속성을 부여하고, 부모 요소에 붙이는 일련의 과정들을 우리는 익숙하게 수행해왔죠.

하지만 리액트에서는 이러한 반복적인 UI 렌더링을 훨씬 더 효율적인 방식으로 처리합니다. 단순히 map() 메서드를 사용하여 데이터 배열을 UI 요소 배열로 변환하는 것만으로도 리스트를 손쉽게 렌더링할 수 있습니다. 여기에 filter와 같은 메서드를 활용하여 데이터를 동적으로 조작할 때, key prop이 어떻게 리액트의 성능 최적화와 안정적인 UI 관리에 기여하는지를 살펴보는 것은 매우 흥미로운 경험이 될 것입니다. 이번 편에서는 우리가 익숙하게 사용했던 자바스크립트의 리스트 렌더링 및 필터링 방식과 리액트의 map() 메서드 및 key prop을 활용한 방식의 차이점을 깊이 있게 탐구하며, 리액트가 어떻게 더 우아하고 효율적인 반복 처리와 동적 데이터 관리를 제공하는지 함께 알아보는 시간을 가질 것입니다.

리액트 고유 기능 - key prop의 중요성과 성능 최적화

자바스크립트엔 없는 리액트의 특징 중 하나는 바로 리스트를 렌더링할 때 사용되는 key prop입니다. key prop은 리액트가 리스트의 각 아이템을 고유하게 식별하는 데 사용되는 특별한 문자열 어트리뷰트입니다. 우리가 바닐라 자바스크립트에서 for 문을 돌려 DOM 요소를 생성할 때는 각 요소의 ‘고유성’을 직접 관리해야 할 필요가 크게 부각되지 않았을 수 있습니다. 하지만 리액트에서는 상태가 변경될 때마다 UI를 효율적으로 업데이트해야 하는데, 이때 key의 역할이 매우 중요해집니다.

리액트는 key를 통해 어떤 아이템이 변경되었는지, 추가되었는지, 혹은 삭제되었는지를 빠르게 파악할 수 있습니다. 예를 들어, 리스트의 순서가 변경되거나 새로운 아이템이 중간에 삽입될 때, key가 없다면 리액트는 변경되지 않은 아이템들조차도 다시 렌더링해야 하는 비효율적인 상황이 발생할 수 있습니다. 특히, 리스트에서 특정 항목을 검색하거나 필터링하여 항목의 순서가 변경되거나 일부 항목이 제거될 때, key가 없다면 리액트는 각 아이템의 변경을 정확히 추적하기 어렵습니다. 하지만 고유한 key가 있다면 리액트는 각 아이템의 이전 상태와 현재 상태를 비교하여 실제로 변경된 부분만 최소한으로 업데이트하게 되므로, 불필요한 DOM 조작을 줄여 성능을 크게 향상시킬 수 있습니다.

따라서 리스트를 렌더링할 때는 반드시 안정적이고 예측 가능한 고유한 key 값을 부여해야 합니다. 일반적으로 데이터베이스의 ID와 같이 변하지 않는 고유한 값을 key로 사용하는 것이 가장 좋습니다. 배열의 인덱스를 key로 사용하는 것은 임시방편일 수는 있지만, 리스트 아이템의 순서가 변경되거나 추가/삭제가 발생할 경우 문제가 발생할 수 있으므로 지양하는 것이 좋습니다. 이 key prop은 단순히 리스트 아이템의 고유성을 보장하는 것을 넘어, 리액트의 핵심 메커니즘인 ‘재조정(Reconciliation)’ 과정에서 효율적인 UI 업데이트를 가능하게 하는 중요한 열쇠라고 생각합니다.

자바스크립트로 리스트 렌더링 만들기

먼저 우리가 늘 사용해온 방식으로, 자바스크립트를 활용하여 간단한 사용자 목록을 DOM에 렌더링하는 방법을 살펴보겠습니다. 이 예제에서는 사용자 데이터 배열을 for 루프를 통해 순회하면서 각 사용자에 대한 <li> 요소를 생성하고, 이를 <ul> 부모 요소에 추가하는 방식으로 동작합니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JavaScript List Rendering</title>
  </head>
  <body>
    <h1>JavaScript 사용자 목록</h1>
    <ul id="user-list">
      <!-- 사용자 목록이 여기에 렌더링됩니다 -->
    </ul>
 
    <script>
      (function () {
        const users = [
          { id: 1, name: "김코딩", age: 30 },
          { id: 2, name: "이개발", age: 25 },
          { id: 3, name: "박리액트", age: 35 },
        ];
 
        const userListElement = document.getElementById("user-list");
 
        function renderUsers() {
          // 기존 목록 초기화 (필요하다면)
          userListElement.innerHTML = "";
          const fragment = document.createDocumentFragment(); // DocumentFragment 생성
 
          users.forEach((user) => {
            const listItem = document.createElement("li");
            listItem.textContent = `ID: ${user.id}, 이름: ${user.name}, 나이: ${user.age}`;
            fragment.appendChild(listItem); // fragment에 추가
          });
          userListElement.appendChild(fragment); // 모든 요소를 한 번에 추가
        }
 
        // 초기 렌더링
        renderUsers();
      })();
    </script>
  </body>
</html>

위 코드를 살펴보면, users 배열의 각 객체에 대해 <li> 요소를 수동으로 생성하고 textContent를 설정한 후, 최종적으로 userListElementappendChild를 사용하여 DOM에 추가하는 것을 확인할 수 있습니다. renderUsers 함수는 UI를 업데이트해야 할 때마다 호출되어 목록을 다시 그리는 역할을 합니다.

자바스크립트 방식의 특징

  1. DOM 직접 조작 (명령형): 개발자가 document.createElement, appendChild 등으로 UI를 '어떻게' 구성할지 상세하게 지시합니다. DocumentFragment를 사용하여 appendChild 호출 횟수를 줄이는 등, 개발자가 수동적인 최적화 노력을 기울일 수 있습니다.
  2. 수동적인 초기 UI 생성: 데이터 배열을 순회하며 각 요소를 직접 만들고 DOM 트리에 추가하여 초기 화면을 그립니다.
  3. 성능 고려사항: DocumentFragment를 활용하여 appendChild 호출 횟수를 줄이는 등의 최적화 노력을 기울일 수 있지만, 여전히 요소가 많아질수록 DOM 조작 비용이 증가할 수 있으며, 리액트의 Virtual DOM 기반 최적화와는 근본적인 차이가 있습니다. 개발자가 직접 성능을 미세 조정해야 하는 부담이 있습니다.
  4. 단순한 변경: 초기 렌더링 후의 단순한 내용 변경(예: 텍스트 수정)은 비교적 간단하지만, 구조적인 변경(순서 변경, 추가/삭제)은 복잡해집니다.

자바스크립트로 리스트 필터링 만들기

이제 자바스크립트에서 리스트 필터링 기능을 추가해 보겠습니다. 사용자가 검색어(searchTerm)를 입력하면 해당 검색어를 포함하는 사용자만 목록에 표시되도록 할 것입니다. 여기서는 고정된 검색어 '김'을 기준으로 필터링하는 예시를 보여드립니다. 이 방식은 keyup 이벤트마다 전체 목록을 다시 렌더링해야 하므로, 목록의 크기가 커질수록 성능 부담이 커질 수 있습니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JavaScript List Filtering</title>
  </head>
  <body>
    <h1>JavaScript 사용자 목록 (필터링)</h1>
    <input type="text" id="search-input" placeholder="이름 검색" value="김" />
    <ul id="user-list-filtered">
      <!-- 사용자 목록이 여기에 렌더링됩니다 -->
    </ul>
 
    <script>
      (function () {
        const allUsers = [
          { id: 1, name: "김코딩", age: 30 },
          { id: 2, name: "이개발", age: 25 },
          { id: 3, name: "박리액트", age: 35 },
          { id: 4, name: "최자바", age: 28 },
          { id: 5, name: "정스크립트", age: 32 },
        ];
 
        const userListElement = document.getElementById("user-list-filtered");
        const searchInput = document.getElementById("search-input");
        const fixedSearchTerm = searchInput.value; // "김"으로 고정
 
        function renderFilteredUsers() {
          userListElement.innerHTML = ""; // 기존 목록 초기화
          const fragment = document.createDocumentFragment(); // DocumentFragment 생성
 
          const filteredUsers = allUsers.filter((user) => user.name.includes(fixedSearchTerm));
 
          filteredUsers.forEach((user) => {
            const listItem = document.createElement("li");
            listItem.textContent = `ID: ${user.id}, 이름: ${user.name}, 나이: ${user.age}`;
            fragment.appendChild(listItem); // fragment에 추가
          });
          userListElement.appendChild(fragment); // 모든 요소를 한 번에 추가
        }
 
        // 초기 렌더링
        renderFilteredUsers();
      })();
    </script>
  </body>
</html>

위 코드는 allUsers 배열에서 fixedSearchTerm('김')을 포함하는 사용자만 필터링하여 새로운 filteredUsers 배열을 생성합니다. 그리고 이 filteredUsers 배열을 기반으로 DOM 요소를 다시 생성하여 userListElement에 추가합니다. 매번 목록을 초기화하고 다시 그리는 과정은 데이터 양이 많아질수록 성능에 큰 영향을 줄 수 있다는 점을 눈여겨볼 필요가 있습니다.

자바스크립트 방식의 특징 (필터링 추가)

  1. 수동적인 필터링 및 전체 재렌더링 방식: 필터링된 결과가 나올 때마다 기존 목록을 모두 지우고(innerHTML = "") DocumentFragment를 활용하여 필터링된 모든 요소를 다시 생성하고 DOM에 추가합니다. DocumentFragment를 통해 appendChild 호출 횟수를 줄이는 수동적인 최적화 노력을 할 수 있습니다.
  2. 동적 UI 변경의 비효율성: 데이터의 작은 변경에도 불구하고 전체 목록을 재생성하므로, 실제 변경이 없는 부분까지 불필요하게 DOM 조작이 발생하여 비효율적입니다.
  3. 성능 저하 가능성: 목록의 크기가 커지거나 필터링이 자주 발생할수록 매번 전체 DOM을 조작하는 비용이 커져 성능 저하로 이어질 가능성이 높습니다.
  4. 개발자 책임 증가: 필터링 과정에서 이전/현재 상태를 비교하여 최소한의 DOM 조작을 하는 로직을 개발자가 직접 구현해야 하는 복잡성이 있으며, DocumentFragment와 같은 추가적인 최적화 기법도 개발자가 직접 적용해야 합니다.

리액트로 동일한 리스트 렌더링 만들기

이제 리액트 버전으로 넘어가 보겠습니다. 리액트에서는 map() 메서드와 JSX를 활용하여 데이터 배열을 UI 요소 배열로 변환하는 방식으로 리스트를 렌더링합니다. key prop의 중요성을 앞에서 살펴보았으니, 이번 예제에서는 각 리스트 아이템에 고유한 key를 부여하는 것을 잊지 않겠습니다.

파일 구조

src/
├── components/
│   └── UserList.jsx
└── App.jsx
└── main.jsx

src/components/UserList.jsx (사용자 목록 컴포넌트)

const users = [
  { id: 1, name: '김코딩', age: 30 },
  { id: 2, name: '이개발', age: 25 },
  { id: 3, name: '박리액트', age: 35 },
];
 
export default function UserList() {
  return (
    <div>
      <h1>React 사용자 목록</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            ID: {user.id}, 이름: {user.name}, 나이: {user.age}
          </li>
        ))}
      </ul>
    </div>
  );
}

UserList 컴포넌트는 users 배열에 저장된 사용자 데이터를 화면에 목록 형태로 렌더링하는 역할을 합니다. 핵심적으로 users.map((user) => (...)) 구문을 사용하여 배열의 각 user 객체를 <li> JSX 요소로 변환하고 있습니다. 이때 각 <li> 요소에 key={user.id}를 부여하여 리액트가 리스트의 각 아이템을 효율적으로 식별하고 관리할 수 있도록 합니다. key prop은 리스트의 아이템이 추가, 삭제, 또는 순서 변경될 때 리액트의 재조정(Reconciliation) 과정에서 최적의 DOM 업데이트를 가능하게 하는 중요한 요소입니다.

src/App.jsx (최상위 앱 컴포넌트)

import UserList from './components/UserList';
 
export default function App() {
  return (
    <div>
      <UserList />
    </div>
  );
}

App 컴포넌트는 애플리케이션의 최상위 컴포넌트로서, 앞서 정의한 UserList 컴포넌트를 임포트(import)하여 렌더링합니다. 이처럼 리액트에서는 작은 단위의 컴포넌트들을 조합하여 전체 UI를 구성하는 방식을 사용하며, 이는 코드의 모듈성(Modularity)과 재사용성을 높이는 데 기여합니다.

src/main.jsx (애플리케이션 진입점 파일)

import ReactDOM from 'react-dom/client';
import App from './App';
 
const rootElement = document.getElementById('root');
if (rootElement) {
  ReactDOM.createRoot(rootElement).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

main.jsx 파일은 리액트 애플리케이션의 시작점입니다. 여기서 ReactDOM.createRoot를 사용하여 HTML 문서의 IDroot인 요소에 리액트 컴포넌트 트리를 마운트(mount)합니다. App 컴포넌트를 <React.StrictMode>로 감싸서 개발 모드에서 잠재적인 문제를 감지하고 경고를 표시하도록 설정하고 있습니다. 이 과정을 통해 우리가 작성한 리액트 컴포넌트들이 브라우저 화면에 최종적으로 렌더링됩니다.

리액트 방식의 특징 (리스트 렌더링과 key)

  • 1. 선언형 리스트 렌더링과 데이터 기반 UI 묘사: 리액트에서는 자바스크립트의 map() 메서드와 JSX를 활용하여 데이터 배열을 선언적으로 UI 요소 배열로 변환합니다. 이는 개발자가 for 루프를 돌며 DOM 요소를 직접 생성하고 추가하는 명령형 방식과 대조됩니다. 개발자는 "이러한 데이터가 있을 때 UI는 이렇게 보여야 한다"는 최종적인 모습(What)을 기술하는 데 집중하고, 리액트가 이 선언된 UI를 실제 DOM에 효율적으로 반영하는 (How) 책임을 집니다. 이는 코드의 가독성을 크게 높이고, 데이터와 UI 간의 관계를 직관적으로 이해할 수 있게 하여 개발자의 인지 부하를 줄여줍니다.
  • 2. key prop의 역할과 렌더링 성능 최적화: 리스트를 렌더링할 때 각 항목에 고유하고 안정적인 key prop을 제공하는 것은 리액트의 성능 최적화안정적인 UI 업데이트에 핵심적인 역할을 합니다. key는 리액트가 리스트의 어떤 항목이 변경(Updated), 추가(Added), 제거(Removed), 또는 순서가 변경(Reordered)되었는지 효율적으로 식별하는 데 도움을 줍니다. key가 없다면 리액트는 변경되지 않은 아이템들까지도 다시 렌더링하거나, 잘못된 상태를 유지하는 등의 비효율적이고 예기치 못한 UI 버그를 유발할 수 있습니다. Virtual DOM재조정(Reconciliation) 과정에서 key는 각 DOM 요소를 고유하게 추적하여 최소한의 DOM 조작만을 수행하게 하므로, 특히 동적인 리스트나 대규모 데이터 목록에서 애플리케이션의 응답성을 크게 향상시킵니다.
  • 3. 간결하고 예측 가능한 코드 및 디버깅 용이성: map(), filter()와 같은 내장 자바스크립트 배열 메서드와 JSX를 함께 사용함으로써, 리스트 렌더링 로직을 매우 간결하고 선언적으로 작성할 수 있습니다. key prop과 불변성 원칙을 준수하면 UI의 동작을 예측하기 쉬워지고, 리스트 내의 특정 항목에 대한 상태 변화나 UI 업데이트가 어떻게 일어나는지 명확하게 추적할 수 있습니다. 이는 복잡한 리스트 UI에서 잠재적인 버그를 줄이고, 개발자가 UI 문제 발생 시 원인을 빠르게 파악하여 해결하는 디버깅 과정을 크게 단순화시킵니다.
  • 4. 컴포넌트 기반 재사용성과 모듈화 촉진: 리스트의 각 아이템을 별도의 재사용 가능한 컴포넌트로 분리하여 렌더링하는 것은 리액트의 모듈화 철학을 강화합니다. 예를 들어 UserList 내에서 UserItem 컴포넌트를 만들고 keyuser prop을 전달하는 방식입니다. 이처럼 각 item을 컴포넌트화하면, 해당 itemUI와 로직이 캡슐화되어 코드의 응집도가 높아지고, 다른 리스트에서도 쉽게 재사용할 수 있게 되어 전체 애플리케이션의 확장성과 유지보수성이 크게 향상됩니다.

리액트 방식으로 리스트 필터링 만들기

이제 리액트에서 필터링 기능을 추가해 보겠습니다. 여기서는 useState Hook을 아직 배우지 않은 독자들을 위해 고정된 검색어('김')를 기준으로 필터링하는 최소한의 예시를 보여드립니다. useState Hook을 사용하면 동적으로 검색어를 받아 필터링할 수 있지만, 이는 3-2편에서 자세히 다룰 예정입니다.

// src/components/FilteredUserList.jsx (별도 파일로 가정)
const allUsers = [
  { id: 1, name: '김코딩', age: 30 },
  { id: 2, name: '이개발', age: 25 },
  { id: 3, name: '박리액트', age: 35 },
  { id: 4, name: '최자바', age: 28 },
  { id: 5, name: '정스크립트', age: 32 },
];
 
const fixedSearchTerm = '김'; // 고정된 검색어
 
export default function FilteredUserList() {
  const filteredUsers = allUsers.filter(user => user.name.includes(fixedSearchTerm));
 
  return (
    <div>
      <h1>React 사용자 목록 (필터링)</h1>
      <input type="text" placeholder="이름 검색" value={fixedSearchTerm} readOnly />
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>
            ID: {user.id}, 이름: {user.name}, 나이: {user.age}
          </li>
        ))}
      </ul>
    </div>
  );
}
// src/App.jsx (최상위 앱 컴포넌트에 추가)
// import UserList from "./components/UserList"; // 이미 임포트되어 있을 수 있음
import FilteredUserList from './components/FilteredUserList';
 
export default function App() {
  return (
    <div>
      <UserList /> {/* 기존 사용자 목록 */}
      <FilteredUserList /> {/* 필터링된 사용자 목록 */}
    </div>
  );
}

위 리액트 코드는 allUsers 배열을 fixedSearchTerm('김')을 기준으로 필터링한 후, 그 결과를 map() 메서드를 통해 <li> JSX 요소 배열로 변환합니다. 여기서 각 <li> 요소에 key={user.id}를 부여하여 리액트가 필터링된 목록의 변경 사항을 효율적으로 추적하고 업데이트할 수 있도록 합니다. useState 없이 고정된 값으로 필터링을 보여주지만, 리액트가 내부적으로 key를 활용하여 얼마나 효율적으로 UI를 업데이트하는지 그 메커니즘을 엿볼 수 있습니다.

자바스크립트 vs 리액트 차이

이제 자바스크립트와 리액트가 리스트 렌더링과 필터링을 어떻게 다루는지 주요 차이점을 비교표로 정리해 보겠습니다. 이 비교를 통해 각 방식의 철학적 차이와 실질적인 구현의 차이를 더욱 명확하게 이해할 수 있을 것입니다.

구분자바스크립트리액트
렌더링 방식for 루프 또는 forEach를 사용하며, DocumentFragment를 활용하여 DOM 요소 직접 생성 및 추가로 효율성 개선 노력 가능map() 메서드를 사용하여 데이터 배열을 JSX 요소 배열로 변환
동적 목록 처리 (필터링)개발자가 filterDocumentFragment를 사용하여 DOM 요소를 직접 생성, 삭제, 추가하여 UI를 수동으로 업데이트하고, 성능 최적화 로직도 직접 구현해야 함filter된 데이터를 map으로 JSX에 바인딩하면, key prop을 통해 리액트가 변경된 부분만 감지하여 효율적으로 DOM을 자동 업데이트함
UI 업데이트수동으로 DOM 조작을 통해 UI 업데이트리액트가 key를 기반으로 변경 사항을 감지하여 효율적으로 업데이트
성능 관리 책임DocumentFragment 등의 기법으로 DOM 업데이트를 최소화하려 노력하지만, 개발자가 수동으로 관리해야 하며 대량의 데이터 처리 시 성능 병목이 발생할 가능성이 높음key prop과 Virtual DOM 메커니즘을 통해 변경된 최소한의 부분만 업데이트하여 성능 최적화가 자동화되며, 개발자의 부담이 줄어듦
책임 분리데이터 관리와 UI 조작 로직이 혼재데이터 변환(map)과 UI 구조 정의(JSX)가 명확히 분리
복잡도동적 리스트 변경 시 수동 DOM 조작으로 복잡도 증가key를 통한 효율적인 관리로 복잡도 감소

요약

이번 편에서는 반복적인 UI 요소를 렌더링하고 동적으로 필터링하는 방식에 있어 자바스크립트와 리액트가 어떤 차이를 가지는지 살펴보았습니다. 우리가 익숙하게 사용했던 자바스크립트 DOM 조작과 리액트의 map() 메서드 및 key prop을 활용한 방식의 특징들을 비교하면서, 리액트가 어떻게 더 효율적이고 간결한 리스트 렌더링과 동적 데이터 관리를 가능하게 하는지 정리해 보았습니다.

  • key prop의 중요성: 리스트 항목 고유 식별, 렌더링 성능 최적화, UI 안정성 확보
  • 선언형 리스트 렌더링: map()JSX 활용, 데이터 기반 UI 묘사, 코드 가독성 향상
  • 성능 및 디버깅: key를 통한 렌더링 효율성 및 예측 가능성으로 디버깅 용이
  • 책임 분리 및 재사용성: 리액트가 DOM 조작 책임 관리, 컴포넌트 재사용 및 모듈화 촉진

다음 편(2-6편)에서는 컴포넌트를 예측 가능하게 만드는 '순수함수 원칙'을 살펴보겠습니다.

참고문서

– [리스트 렌더링]