React|성능 최적화와 보안

대용량 데이터 렌더링: 가상화(Virtualization - React Window)로 DOM 노드 갯수 유지하기

0
대용량 데이터 렌더링: 가상화(Virtualization - React Window)로 DOM 노드 갯수 유지하기

크리스가 관리자 대시보드를 만들고 있다. 이번 미션은 '전체 사용자 10,000명의 로그'를 리스트로 보여주는 것이다.

별생각 없이 map 함수로 10,000개의 div를 그렸다.

// ❌ 브라우저를 죽이는 코드
{logs.map(log => <LogItem key={log.id} data={log} />)}

새로고침을 하는 순간, 브라우저는 얼어붙었다. 스크롤은 뚝뚝 끊기고, 마우스 클릭도 먹통이다.

아무리 리액트가 빨라도, DOM 노드 10,000개를 한 번에 그리고 관리하는 것은 무리다. 브라우저가 위치를 계산(Reflow)하고 색칠(Repaint)해야 할 면적이 너무 넓기 때문이다.

이 문제를 해결하기 위해 가상화(Virtualization), 혹은 윈도잉(Windowing)이라 불리는 기술을 사용해야 한다.

1. 윈도잉(Windowing)의 원리

원리는 간단하다. "눈에 보이는 것만 그리자"는 것이다.

데이터가 10,000개라도 사용자의 모니터 화면(Viewport)에 들어오는 건 고작 10~20개뿐이다.

  • 나머지 9,980개는 아예 렌더링 하지 않는다. (DOM에 존재하지 않음)
  • 대신 스크롤바의 크기를 유지하기 위해 위아래에 거대한 빈 공간(Padding)을 넣어준다.
  • 사용자가 스크롤을 내리면, 그 위치에 맞는 아이템으로 내용을 갈아 끼운다.
  • 결과적으로 DOM에는 항상 20개 내외의 노드만 존재한다. 데이터가 10만 개, 100만 개가 되어도 브라우저는 쾌적하다.

    2. 라이브러리 선택: react-window

    이 복잡한 스크롤 계산 로직을 직접 구현할 필요는 없다. 리액트 팀 멤버가 만든 검증된 라이브러리인 react-window를 사용하면 된다. (과거에 쓰던 react-virtualized보다 훨씬 가볍고 빠르다.)

    기본 사용법 (FixedSizeList)

    아이템의 높이가 고정되어 있다면 FixedSizeList를 사용한다.

    // LogList.tsx
    import { FixedSizeList as List } from 'react-window';
    
    // 1. 각 행(Row)을 그릴 컴포넌트
    // style을 반드시 props로 받아서 적용해야 위치가 잡힌다.
    const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
      <div style={style} className="log-item">
        Log #{index} - {logs[index].message}
      </div>
    );
    
    function LogList() {
      return (
        <List
          height={500}        // 리스트 전체 높이 (px)
          itemCount={10000}   // 전체 아이템 개수
          itemSize={50}       // 아이템 하나당 높이 (px)
          width={300}         // 리스트 전체 너비 (px)
        >
          {Row}
        </List>
      );
    }

    이제 10,000개의 로그가 있어도 DOM에는 딱 화면 크기만큼만 그려진다.

    3. 반응형 너비 대응하기 (AutoSizer)

    react-window는 width와 height에 고정된 픽셀값(number)을 요구한다.

    하지만 우리는 반응형 웹을 만든다. 리스트가 화면 꽉 차게(100%) 늘어나야 한다면 어떻게 해야 할까?

    이때는 react-virtualized-auto-sizer라는 별도의 패키지를 함께 사용해야 한다. 부모 컨테이너의 크기를 측정해서 widthheight를 넘겨주는 역할을 한다.

    // ResponsiveLogList.tsx
    import AutoSizer from 'react-virtualized-auto-sizer';
    import { FixedSizeList as List } from 'react-window';
    
    function ResponsiveLogList() {
      return (
        <div style={{ flex: 1, height: '100vh' }}>
          <AutoSizer>
            {({ height, width }) => (
              <List
                height={height} // 부모 높이를 받아옴
                width={width}   // 부모 너비를 받아옴
                itemCount={10000}
                itemSize={50}
              >
                {Row}
              </List>
            )}
          </AutoSizer>
        </div>
      );
    }

    4. 주의할 점과 한계

    가상화는 만능열쇠가 아니다. 몇 가지 제약 사항이 있다.

    1. 높이 계산 문제 (VariableSizeList)

    채팅 메시지처럼 아이템마다 높이가 제각각이라면? itemSize에 숫자가 아니라 함수를 넘겨서 계산해야 한다. 하지만 텍스트 줄바꿈 등으로 높이를 예측하기 어렵다면 구현 난이도가 급상승한다.

    2. 검색(Ctrl+F) 문제

    브라우저의 기본 찾기 기능(Ctrl+F)은 DOM에 있는 텍스트만 찾을 수 있다. 가상화된 리스트는 화면 밖의 텍스트가 DOM에 없으므로 검색되지 않는다. 따라서 별도의 검색 기능을 구현해야 한다.

    3. 접근성 및 SEO

    스크린 리더나 검색 엔진 봇도 빈 껍데기만 볼 수 있다. SEO가 중요한 페이지라면 가상화를 신중하게 적용해야 한다.

    핵심 정리

  • 문제: 대량의 데이터를 렌더링 하면 DOM 노드가 많아져서 브라우저 성능이 급격히 저하된다.
  • 해결: react-window를 사용하여 화면에 보이는 부분만 렌더링 한다(가상화).
  • 구현: FixedSizeList가 기본이며, 반응형 대응을 위해 AutoSizer를 함께 사용한다.
  • 제약: 브라우저 기본 검색(Ctrl+F)이 안 되며, 동적 높이 계산이 까다롭다.
  • 지금까지 성능 최적화 기술들을 다뤘다. 이제 앱을 안전하게 지키는 보안(Security) 영역으로 넘어가자.

    로그인을 구현할 때, 토큰을 어디에 저장해야 할까? 로컬 스토리지? 쿠키?

    가장 안전한 인증 전략인 액세스 토큰과 리프레시 토큰의 댄스를 배워본다.

    Part 4의 세 번째 주제, "Access Token과 Refresh Token: 보안과 UX 사이의 안전한 인증 전략" 편에서 계속된다.


    🔗 참고 링크

  • react-window GitHub
  • react-virtualized-auto-sizer
  • Web.dev - Virtualize long lists
  • 댓글 (0)

    0/1000
    댓글을 불러오는 중...