PromleeBlog
sitemap
aboutMe

posting thumbnail
React와 자료구조 (배열, 리스트, 트리, 해시 테이블) - React, 알고 쓰자 8일차
React & Data Structures (Arrays, Lists, Trees, Hash Tables) - React Explained Day 8

📅

🚀

들어가기 전에 🔗

지금까지 우리는 React의 다양한 기능과 렌더링 방식에 대해 집중적으로 탐구했습니다. 오늘은 조금 다른 관점에서 React를 바라보려고 합니다.
바로 컴퓨터 과학의 기본 중의 기본,
자료구조(Data Structures)
입니다.
"프론트엔드 개발자가 자료구조를 알아야 하나요?" 라고 생각하실 수도 있습니다.
하지만 우리가 작성하는 React 코드, 그리고 React 라이브러리 자체의 내부 동작 방식 깊숙한 곳에는 다양한 자료구조가 효율성을 위해 사용되고 있습니다.
자료구조에 대한 이해는 단순히 이론적인 지식을 넘어, 우리가 왜 특정 방식으로 코드를 작성해야 하는지(예: 배열 업데이트 시 불변성 유지), React가 내부적으로 어떻게 상태를 관리하고 UI를 업데이트하는지 더 깊이 이해하는 데 큰 도움을 줍니다.
오늘은 React와 밀접하게 관련된 주요 자료구조들을 살펴보고, 이것들이 실제 개발과 React 내부에 어떻게 녹아 있는지 함께 알아보겠습니다.

🚀

배열 (Arrays) 🔗

배열은 아마도 React 개발에서 가장 흔하게 접하는 자료구조일 것입니다.
특히, 여러 개의 아이템 목록을 화면에 보여줘야 할 때 배열은 필수적으로 사용됩니다.

.map()을 이용한 리스트 렌더링 🔗

React 컴포넌트에서 배열 데이터를 사용하여 UI 목록을 생성할 때, JavaScript 배열의 map 메서드를 사용하는 것이 가장 일반적입니다.
map 메서드는 배열의 각 요소를 순회하면서 주어진 함수를 실행하고, 그 결과로 새로운 배열을 만들어 반환합니다.
React는 이 새로운 배열(주로 JSX 요소들로 구성된)을 받아 화면에 렌더링합니다.
import React from 'react';
 
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
 
const initialTodos: Todo[] = [
  { id: 1, text: 'React 공부하기', completed: true },
  { id: 2, text: '자료구조 복습하기', completed: false },
  { id: 3, text: '블로그 글쓰기', completed: false },
];
 
function TodoList() {
  const [todos, setTodos] = React.useState<Todo[]>(initialTodos);
 
  return (
    <ul>
      {/* 배열의 map 함수를 사용하여 li 요소 배열 생성 */}
      {todos.map(todo => (
        // 각 항목에 고유한 key prop 전달이 필수!
        <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

key prop과 diff 알고리즘의 중요성 🔗

참고: React 렌더링 원리 파헤치기 Virtual DOM과 Fiber - React, 알고 쓰자 1일차
map을 사용하여 리스트를 렌더링할 때, 각 요소에 고유하고 안정적인 key prop을 제공하는 것이 매우 중요합니다.
우리가 1일차에 배웠던 React의 재조정(Reconciliation) 과정, 즉 이전 Virtual DOM 트리와 새로운 Virtual DOM 트리를 비교하는
diffing 알고리즘
은 이 key prop을 사용하여 어떤 항목이 추가, 삭제, 또는 이동되었는지 효율적으로 파악합니다.
만약 key prop이 없거나 배열의 인덱스를 key로 사용하면, 리스트 중간에 항목이 추가되거나 삭제될 때 React는 변경 사항을 정확히 추적하기 어려워 불필요한 DOM 조작을 수행할 수 있습니다. 이는 성능 저하로 이어질 수 있습니다.
항상 데이터 자체의 고유 ID를 key로 사용하는 것이 좋습니다.

불변성 유지의 중요성 🔗

React에서 상태 업데이트 시
불변성(Immutability)
을 유지하는 것은 매우 중요합니다.
배열 상태를 업데이트할 때는 원본 배열을 직접 수정하는 push, pop, splice 등의 메서드 대신, 새로운 배열을 반환하는 map, filter, reduce나 스프레드 연산자(...)를 사용해야 합니다.
React는 상태 객체나
배열의 참조
(주소값)가 변경되었는지를 비교하여 리렌더링 여부를 결정하는 경우가 많기 때문입니다(얕은 비교).
원본 배열을 직접 수정하면 참조가 변경되지 않아 React가 변화를 감지하지 못하고 리렌더링이 일어나지 않을 수 있습니다.
// ❌ 잘못된 예시: 원본 배열 직접 수정
newTodos.push({ id: 4, text: '새 할일', completed: false });
setTodos(newTodos); // React가 변화 감지 못할 수 있음
 
// ✅ 올바른 예시: 스프레드 연산자로 새 배열 생성
setTodos([...todos, { id: 4, text: '새 할일', completed: false }]);
 
// ✅ 올바른 예시: filter로 항목 삭제
setTodos(todos.filter(todo => todo.id !== idToRemove));
 
// ✅ 올바른 예시: map으로 특정 항목 업데이트
setTodos(todos.map(todo =>
  todo.id === idToToggle ? { ...todo, completed: !todo.completed } : todo
));

🚀

연결 리스트 (Linked Lists) 🔗

연결 리스트는 배열과 달리 메모리상에 연속적으로 위치하지 않고, 각 요소(노드)가 데이터와 다음 요소를 가리키는 참조(포인터)를 가지는 형태로 연결된 자료구조입니다.
React 개발 시 직접 연결 리스트를 구현하는 경우는 드물지만, React의
내부 동작 원리
를 이해하는 데 매우 중요합니다.

Hooks 연결 리스트 (Fiber 노드) 🔗

우리가 2일차에 살펴봤듯이, React는 함수형 컴포넌트에서 사용된 훅(useState, useEffect 등)들의 상태를 관리하기 위해 내부적으로 연결 리스트 구조를 사용합니다.
Hook 객체 연결 리스트
Hook 객체 연결 리스트

Update Queue 연결 리스트 (상태 업데이트) 🔗

useState나 useReducer의 상태 업데이트 함수(setState, dispatch)가 호출되면, React는 즉시 상태를 변경하지 않고
업데이트 요청(Update)
객체를 생성하여 해당 Hook 객체의
Update Queue
에 추가한다고 배웠습니다 (2일차).
이 Update Queue 역시
단일 연결 리스트(Singly Linked List)
구조로 구현되어 있습니다.
Q: "React Hooks는 내부적으로 상태를 어떻게 관리하나요?" or "useState의 업데이트는 어떻게 처리되나요?"
A: Fiber 노드의 Hook 연결 리스트나 Update Queue 연결 리스트 구조를 언급하며 답변.

🚀

트리 (Trees): React 구조의 근간 🔗

트리는 부모-자식 관계를 가지는 노드들로 이루어진 계층적 자료구조입니다.
React 애플리케이션의 구조와 렌더링 과정을 이해하는 데 트리는 빼놓을 수 없는 핵심 개념입니다.

컴포넌트 트리 (Component Tree) 🔗

우리가 작성하는 React 코드는 기본적으로 컴포넌트들의 중첩된 구조, 즉
컴포넌트 트리
를 형성합니다.
최상위 App 컴포넌트부터 시작해서 페이지, 레이아웃, 각 UI 요소 컴포넌트들이 부모-자식 관계를 이루며 계층적으로 구성됩니다.
이 컴포넌트 트리는 애플리케이션의 UI 구조를 추상적으로 표현합니다.

DOM 트리 (DOM Tree) 🔗

웹 브라우저는 HTML 문서를 파싱하여
DOM(Document Object Model) 트리
라는 객체 모델을 메모리에 생성합니다.
이 DOM 트리는 실제 웹 페이지의 구조를 나타내며, 각 HTML 요소는 트리 노드가 됩니다.
브라우저는 이 DOM 트리를 기반으로 화면에 UI를 그립니다.

Virtual DOM 트리 (Virtual DOM Tree) 🔗

React는 실제 DOM 트리를 직접 조작하는 대신, 메모리 상에 가상의 DOM 트리, 즉
Virtual DOM 트리
를 유지합니다 (1일차).
이 Virtual DOM 트리는 JavaScript 객체로 표현되어 실제 DOM 트리보다 훨씬 가볍고 빠르게 조작할 수 있습니다.
상태가 변경되면 React는 새로운 Virtual DOM 트리를 생성하고, 이전 Virtual DOM 트리와 비교(diffing)하여 변경된 부분만 실제 DOM 트리에 효율적으로 반영합니다.

재조정과 트리 순회 🔗

React의 재조정(Reconciliation) 과정은 본질적으로 두 개의 트리(이전 Virtual DOM 트리와 새로운 Virtual DOM 트리)를 비교하는 작업입니다.
React는 이 비교를 효율적으로 수행하기 위해
트리 순회(Tree Traversal)
알고리즘(
주로 깊이 우선 탐색(DFS)
방식)을 기반으로 노드들을 비교합니다.
같은 레벨의 노드들을 비교하고, 자식 노드들을 재귀적으로 탐색하면서 변경 사항을 찾아냅니다.
key prop은 같은 레벨의 형제 노드들 사이에서 비교 효율성을 높이는 데 중요한 역할을 합니다.

🚀

그래프 (Graphs): 관계와 의존성 표현 🔗

그래프는 노드(정점, Vertex)와 그 노드들을 연결하는 간선(엣지, Edge)으로 구성된 자료구조입니다.
객체 간의 복잡한 관계나 의존성을 표현하는 데 유용하며, React 개발 생태계에서도 다양한 방식으로 활용됩니다.

의존성 그래프 (Dependency Graph) 🔗

src/
├── App.js  - Header, Footer, Home 참조
├── components/
│   ├── Header.js - Nav 참조
│   ├── Footer.js - Button 참조
│   ├── Nav.js
│   └── Button.js
├── pages/
└   └── Home.js - Button 참조
의존성 그래프 예시
        App.js
     /     |      \
Header  Footer  pages
  |        |      |
 Nav    Button - Home   

코드 스플리팅과 청크 그래프 🔗

React.lazy를 이용한 코드 스플리팅 시, 번들러는 코드를 여러 개의 작은 조각, 즉
청크(chunk)
파일로 나눕니다.
이 청크들 사이에도 의존 관계가 존재할 수 있습니다 (예: 특정 페이지 청크는 공통 라이브러리 청크에 의존).
번들러는 이 청크 간의 의존성을 그래프 형태로 관리하여, 특정 페이지를 로드할 때 필요한 모든 의존 청크들을 함께 로드하도록 처리합니다.

🚀

해시 테이블 (Hash Tables / Maps) 🔗

해시 테이블(또는 해시 맵)은 키(Key)를 고유한 해시 값으로 변환하고, 이 해시 값을 인덱스로 사용하여 값(Value)을 저장하거나 검색하는 자료구조입니다.
평균적으로 매우 빠른 검색, 삽입, 삭제 시간 복잡도(O(1))를 가지는 것이 특징입니다.
JavaScript에서는 Map 객체나 일반 객체(Object)를 통해 유사한 기능을 활용할 수 있습니다.

상태 캐싱 (Caching) 🔗

React Query, SWR과 같은 서버 상태 관리 라이브러리들은 API 요청 결과를 효율적으로 캐싱하기 위해 내부적으로 해시 테이블과 유사한 구조를 사용합니다.

메모이제이션 (Memoization) 🔗

React의 성능 최적화 기법 중 하나인 메모이제이션(Memoization)에도 해시 테이블/Map 구조가 활용됩니다.
메모이제이션은 이전에 계산한 결과를 저장해두고, 동일한 입력에 대해서는 계산 과정 없이 저장된 결과를 반환하는 기술입니다.

🚀

결론 🔗

오늘은 React 개발과 밀접하게 관련된 주요 자료구조들(배열, 연결 리스트, 트리, 그래프, 해시 테이블)을 살펴보았습니다.
자료구조에 대한 이해는 단순히 알고리즘 문제를 푸는 것을 넘어, 우리가 사용하는 React라는 도구가 어떻게 더 효율적으로 동작하는지, 그리고 우리가 작성하는 코드가 성능에 어떤 영향을 미치는지 더 깊이 있게 파악하는 데 도움을 줍니다.
다음 시간에는 좋은 React
컴포넌트를 설계하는 원칙과 패턴
에 대해 알아보겠습니다.
단순히 동작하는 코드를 넘어, 재사용 가능하고 테스트하기 쉬우며 유지보수하기 좋은 컴포넌트를 만드는 방법에 대해 함께 고민해보는 시간을 갖겠습니다.

참고 🔗