React Compiler 도입 전 체크리스트: useMemo와 useCallback을 지우기 전에 볼 것들

2026. 6. 2. 14:44카테고리 없음

반응형

React 프로젝트에서 성능 최적화를 하다 보면 useMemo, useCallback, React.memo가 자연스럽게 늘어납니다. 처음에는 느린 계산을 줄이기 위한 선택이었는데, 어느 순간부터는 “혹시 모르니 감싸두자”에 가까운 코드가 됩니다. 리뷰에서도 실제 병목보다 dependency array가 맞는지, 콜백 참조가 바뀌는지 같은 이야기로 시간이 많이 쓰입니다.

React Compiler는 이 흐름을 바꿀 수 있는 도구입니다. React가 컴포넌트와 훅의 코드를 분석해 안전하다고 판단되는 부분을 자동으로 메모이제이션해 주기 때문입니다. 공식 문서에서도 React Compiler는 수동 useMemo, useCallback, React.memo 사용을 줄이는 방향의 자동 최적화 도구로 설명됩니다.

하지만 여기서 바로 “이제 useMemo는 전부 지워도 된다”로 가면 위험합니다. Compiler는 마법처럼 모든 성능 문제를 해결하는 기능이 아니라, React 규칙을 잘 지키는 코드에서 더 좋은 기본값을 만들어 주는 도구에 가깝습니다. 이번 글에서는 React Compiler를 도입하기 전에 어떤 기준으로 코드를 살펴보고, 기존 메모이제이션을 어떤 순서로 정리하면 좋은지 정리해보겠습니다.

React Compiler가 해결하려는 문제

React에서 렌더링 성능 문제는 보통 세 가지 형태로 나타납니다.

첫째, 부모 컴포넌트가 렌더링될 때 자식 컴포넌트도 불필요하게 다시 렌더링됩니다. 둘째, 렌더링 중 비싼 계산이 반복됩니다. 셋째, 객체나 함수 참조가 매번 바뀌어서 memo로 감싼 컴포넌트도 계속 다시 렌더링됩니다.

그래서 기존에는 다음과 같은 방식으로 대응했습니다.

const filteredItems = useMemo(() => {
  return items.filter((item) => item.enabled);
}, [items]);

const handleSelect = useCallback((id: string) => {
  setSelectedId(id);
}, []);

이 방식은 명시적입니다. 개발자가 어떤 값을 기억할지 직접 정합니다. 문제는 코드가 커질수록 “정말 필요한 메모이제이션”과 “습관적으로 붙인 메모이제이션”이 섞인다는 점입니다. dependency array를 잘못 쓰면 오래된 값을 보거나, 반대로 필요 없는 의존성을 넣어 최적화 효과가 사라질 수도 있습니다.

React Compiler는 이 부담을 줄이기 위해 만들어졌습니다. 컴파일 단계에서 컴포넌트와 훅의 순수성을 분석하고, 안전한 경우 React가 자동으로 재계산과 재렌더링을 줄입니다. 즉 핵심은 단순히 훅 하나를 대체하는 것이 아니라, React 코드가 예측 가능하게 작성되어 있을 때 최적화를 자동화하는 것입니다.

도입 전에 먼저 확인할 코드 조건

React Compiler를 적용하기 전에 가장 먼저 볼 것은 번들 설정이 아니라 코드의 형태입니다. Compiler는 React의 규칙을 전제로 동작합니다. 컴포넌트와 훅이 순수하게 작성되어 있고, 렌더링 중 외부 상태를 임의로 바꾸지 않으며, 훅 호출 순서가 안정적이어야 합니다.

다음 패턴이 많다면 먼저 정리하는 편이 좋습니다.

  • 렌더링 중 전역 변수나 외부 객체를 수정한다.
  • 조건문 안에서 훅을 호출한다.
  • 컴포넌트 렌더링 중 네트워크 요청이나 저장소 쓰기 같은 부수 효과를 실행한다.
  • props로 받은 객체를 직접 변경한다.
  • dependency array 경고를 무시하기 위해 ESLint 규칙을 끈다.
  • 커스텀 훅 안에서 값의 소유권이 불분명하다.

예를 들어 아래 코드는 렌더링 중 외부 배열을 수정합니다.

const logs: string[] = [];

function UserCard({ user }: { user: User }) {
  logs.push(user.id);

  return <div>{user.name}</div>;
}

이런 코드는 Compiler 이전에도 좋지 않습니다. React 컴포넌트는 같은 입력에 대해 같은 UI를 반환해야 이해하기 쉽습니다. 렌더링 과정에서 외부 상태를 바꾸면 Strict Mode, 동시성 렌더링, 캐시, 자동 최적화와 모두 충돌할 수 있습니다.

도입 전 체크리스트를 짧게 만들면 다음과 같습니다.

점검 항목 확인할 질문

순수성 렌더링 중 외부 상태를 변경하지 않는가?
훅 규칙 훅 호출 순서가 항상 같은가?
부수 효과 네트워크 요청, 구독, DOM 조작이 useEffect 등으로 분리되어 있는가?
불변성 props와 state를 직접 수정하지 않는가?
린트 React Hooks 관련 경고를 무시하지 않는가?

Compiler를 성능 도구로만 보면 이 단계가 귀찮아 보입니다. 하지만 실제로는 이 정리만 해도 코드 품질이 좋아집니다. 자동 최적화는 그 다음에 얻는 보너스에 가깝습니다.

useMemo와 useCallback을 바로 삭제하지 않는다

React Compiler를 켰다고 해서 기존 useMemo와 useCallback을 한 번에 제거할 필요는 없습니다. 특히 실무 프로젝트에서는 성능 문제가 발생한 이유를 기록 없이 지워버리면 나중에 같은 문제를 다시 추적해야 합니다.

먼저 메모이제이션을 세 가지로 나누어 보는 것이 좋습니다.

  1. 실제로 비싼 계산을 줄이기 위한 메모이제이션
  2. 자식 컴포넌트의 불필요한 렌더링을 줄이기 위한 참조 안정화
  3. 습관적으로 붙였지만 효과가 불분명한 메모이제이션

첫 번째는 신중하게 다뤄야 합니다. 예를 들어 수천 개 항목을 정렬하거나, 큰 데이터를 그룹핑하는 계산은 여전히 명시적인 구조가 도움이 될 수 있습니다. Compiler가 최적화를 해주더라도, 해당 계산이 정말 병목인지 측정하고 남길지 결정하는 편이 안전합니다.

두 번째는 Compiler 도입 후 줄어들 가능성이 큽니다. 단지 React.memo 자식에게 넘기기 위해 모든 콜백을 useCallback으로 감싸는 패턴은 점차 줄일 수 있습니다. 다만 삭제 전후로 React DevTools Profiler나 실제 사용자 지표를 확인하는 것이 좋습니다.

세 번째는 정리 후보입니다.

const title = useMemo(() => post.title, [post.title]);

const onClick = useCallback(() => {
  console.log('clicked');
}, []);

이런 코드는 최적화보다 코드 소음에 가깝습니다. Compiler가 없더라도 제거해도 되는 경우가 많습니다. 하지만 한꺼번에 대량 삭제하기보다는 화면 단위, 기능 단위로 나누어 변경하는 편이 리뷰와 롤백이 쉽습니다.

마이그레이션 순서: 설정보다 측정이 먼저다

React Compiler 도입을 프로젝트 일정에 넣는다면 다음 순서가 현실적입니다.

1. 성능 기준을 먼저 정한다

Compiler를 켜기 전 현재 상태를 기록합니다. 모든 페이지를 완벽하게 측정할 필요는 없지만, 핵심 화면 몇 개는 기준을 잡아두는 것이 좋습니다.

  • 목록 페이지에서 필터 변경 시 렌더링 횟수
  • 입력 폼에서 타이핑 지연 여부
  • 대시보드 첫 렌더링 시간
  • 큰 테이블 정렬이나 검색 시 반응 속도
  • React Profiler에서 자주 렌더링되는 컴포넌트

기준이 없으면 Compiler 적용 후 좋아졌는지, 나빠졌는지, 그대로인지 말하기 어렵습니다. “빌드는 성공했다”와 “실제 성능이 안정적이다”는 다른 이야기입니다.

2. 린트와 React 규칙 위반을 먼저 줄인다

Compiler가 잘 동작하려면 코드가 React의 기대에 맞아야 합니다. 특히 Hooks 규칙, exhaustive deps, 불변성 관련 경고를 무시하고 있다면 먼저 정리합니다. 이 과정에서 오래된 커스텀 훅의 책임도 함께 드러납니다.

3. 작은 범위에서 Compiler를 켠다

가능하다면 전체 앱이 아니라 일부 패키지나 화면부터 적용합니다. 모노레포라면 UI 패키지 하나, 서비스라면 트래픽이 낮은 관리 화면부터 시작할 수 있습니다. 실패했을 때 영향 범위가 작아야 원인을 찾기 쉽습니다.

4. 수동 메모이제이션은 변경 단위별로 정리한다

Compiler를 켠 PR과 useMemo 대량 삭제 PR을 분리하는 것이 좋습니다. 하나의 PR에서 설정 변경, 린트 수정, 메모이제이션 삭제, 리팩토링이 동시에 일어나면 문제가 생겼을 때 원인을 찾기 어렵습니다.

추천 흐름은 이렇습니다.

  1. Compiler 적용을 위한 설정과 린트 정리
  2. 테스트와 주요 화면 동작 확인
  3. 불필요한 useMemo, useCallback을 화면 단위로 제거
  4. 성능 지표와 렌더링 횟수 비교
  5. 문제가 있으면 해당 변경만 되돌림

이렇게 하면 Compiler 도입이 “큰 기술 부채 정리 이벤트”가 아니라 반복 가능한 마이그레이션 작업이 됩니다.

지워도 되는 메모이제이션과 남겨둘 메모이제이션

실무에서는 다음 기준으로 판단하면 편합니다.

지워도 되는 경우는 보통 이렇습니다.

  • 계산 비용이 거의 없는 단순 값 조합
  • props로 내려가지 않는 내부 콜백
  • dependency array를 맞추기 위해 코드가 더 복잡해진 경우
  • Profiler에서 효과가 확인되지 않은 방어적 최적화
  • React.memo와 세트로 붙어 있지만 실제 렌더링 감소가 없는 경우

반대로 남겨둘 수 있는 경우도 있습니다.

  • 큰 배열 정렬, 필터링, 그룹핑처럼 계산 비용이 분명한 경우
  • 외부 라이브러리에 안정적인 참조를 넘겨야 하는 경우
  • dependency 변경이 의미 있는 캐시 경계인 경우
  • 성능 이슈를 해결한 이력이 있고 측정 근거가 남아 있는 경우
  • Compiler 적용 범위 밖의 코드와 맞물려 있는 경우

중요한 것은 “Compiler가 있으니 전부 삭제”도 아니고, “혹시 모르니 전부 유지”도 아닙니다. 메모이제이션이 코드의 의미를 더 명확하게 만드는지, 아니면 불안해서 붙인 장식인지 구분해야 합니다.

흔한 실수

React Compiler를 도입할 때 자주 나오는 실수는 다음과 같습니다.

  • Compiler 적용과 대규모 리팩토링을 같은 PR에 넣는다.
  • useMemo, useCallback 삭제만 목표로 삼는다.
  • 성능 측정 없이 “최신 기능을 썼으니 빨라졌을 것”이라고 판단한다.
  • React 규칙 위반 경고를 그대로 둔 채 Compiler만 켠다.
  • 서버 상태, 네트워크 지연, 번들 크기 문제까지 Compiler가 해결한다고 기대한다.

Compiler는 렌더링 최적화 도구입니다. API 응답이 느리거나, 이미지가 크거나, 서버 컴포넌트 경계가 잘못되어 있거나, 상태 관리 구조가 복잡한 문제까지 대신 해결해 주지는 않습니다. 성능 문제를 만났을 때는 여전히 병목을 나누어 봐야 합니다.

결론: 자동 최적화보다 중요한 것은 예측 가능한 코드다

React Compiler의 가장 큰 장점은 개발자가 성능을 매번 수동으로 챙겨야 하는 부담을 줄여준다는 점입니다. 특히 참조 안정화를 위해 반복적으로 붙이던 useCallback, 단순 계산을 감싸던 useMemo, 방어적인 React.memo 사용은 줄어들 가능성이 큽니다.

하지만 도입의 핵심은 훅을 몇 개 지우느냐가 아닙니다. 컴포넌트가 순수하고, 상태의 소유권이 분명하고, 부수 효과가 올바른 위치에 있고, 성능을 측정할 수 있는 상태인지가 더 중요합니다.

정리하면 실무 기준은 이렇습니다.

  • Compiler를 켜기 전에 React 규칙 위반부터 줄인다.
  • 현재 성능 기준을 기록한다.
  • 작은 범위에서 적용하고 관찰한다.
  • 수동 메모이제이션은 효과가 불분명한 것부터 제거한다.
  • 비싼 계산이나 외부 라이브러리 경계는 측정 후 결정한다.

React Compiler는 좋은 React 코드를 더 편하게 유지하게 해주는 도구입니다. 그래서 도입 전 가장 먼저 할 일은 새로운 설정을 찾는 것이 아니라, 우리 코드가 자동 최적화를 받아들일 만큼 예측 가능한지 확인하는 것입니다.

Reference

반응형