React useMemo 꼭 사용해야할까?

ReactNext.js

2025. 10. 13.

React Compiler: 메모이제이션 트렌드

"useMemo와 useCallback을 꼭 사용해야 할까?"

React 개발자라면 한 번쯤 고민해봤을 문제입니다. 그 문제 해결에 도움이 될 React Compiler를 소개합니다.

TL;DR

  • React Compiler는 빌드 타임에 자동으로 메모이제이션을 적용하는 도구
  • Next.js 16에서 안정화되어 프로덕션 사용 가능
  • Meta Quest Store에서 초기 로드 최대 12% 향상, 특정 인터랙션 2.5배 빠름
  • 과도한 수동 메모이제이션은 오히려 생산성을 31-46% 저하
  • 당분간은 "성능 문제가 있을 때만 최적화" 원칙 유지

배경: 메모이제이션의 딜레마

React 개발을 하다 보면 필연적으로 만나는 고민이 있습니다.

1function ProductList({ products }) {
2  const sortedProducts = products.sort((a, b) => a.price - b.price);
3
4  const handleClick = (id) => {
5    console.log(id);
6  };
7
8  return (
9    <div>
10      {sortedProducts.map(p => (
11        <ProductCard
12          key={p.id}
13          product={p}
14          onClick={handleClick}
15        />
16      ))}
17    </div>
18  );
19}
20

이 코드를 메모이제이션 해야 할까요?

"최적화를 위해 useMemouseCallback을 써야 하나?"라는 질문에 팀마다 다른 답을 가지고 있습니다:

  • A: "모든 걸 메모이제이션하자!"
  • B: "성능 문제가 생기면 그때 추가하자"
  • C: "...뭐가 맞는지 모르겠다"

그리고 많은 경우, A팀의 코드베이스는 점점 유지보수 지옥이 되어갑니다.


React Compiler의 등장

React Compiler(구 React Forget)는 빌드 타임에 자동으로 메모이제이션을 적용하는 Babel 플러그인입니다.

핵심 아이디어: 개발자는 평범한 React 코드를 작성하고, 컴파일러가 알아서 최적화합니다.

1function ExpensiveComponent({ data, onClick }) {
2  const processedData = data.map(item => transform(item));
3
4  const handleClick = (id) => {
5    onClick(id);
6  };
7
8  return (
9    <div>
10      {processedData.map(item => (
11        <Item key={item.id} onClick={() => handleClick(item.id)} />
12      ))}
13    </div>
14  );
15}
16

위 코드를 작성하면, 컴파일러가 자동으로:

  • processedDatadata가 변경될 때만 재계산
  • handleClick의 참조 유지
  • 불필요한 리렌더링 방지

작동 원리

React Compiler는 단순히 모든 것을 useMemo/useCallback으로 감싸는 게 아닙니다:

  1. 데이터 흐름 분석: AST를 자체 HIR(High-level Intermediate Representation)로 변환
  2. 세밀한 의존성 추적: 어떤 값이 언제 변경되는지 정확히 파악
  3. 조건부 메모이제이션: 수동으로는 불가능한 최적화 적용
  4. Rules of React 검증: 숨어있던 버그까지 발견

결과적으로 개발자가 수동으로 하는 것보다 더 정교하고 정확한 최적화가 가능합니다.


Next.js 16에서 사용하기

현재 상태 (2025년 10월)

Next.js 16은 현재 베타 단계이며, React Compiler가 안정화되어 포함되었습니다. 2025년 10월 9일에 발표된 이번 업데이트는 Turbopack의 안정화와 함께 React Compiler를 프로덕션 환경에서 사용할 수 있게 만들었습니다.

설치 및 설정

npm install babel-plugin-react-compiler@latest

설정 파일

1const nextConfig = {
2  reactCompiler: true,
3  experimental: {
4    turbopackFileSystemCacheForDev: true,
5  },
6};
7
8export default nextConfig;

주의사항

  • 기본적으로 비활성화되어 있어 명시적 활성화 필요
  • Babel 의존성으로 인한 빌드 시간 증가
  • 베타 버전이므로 프로덕션 적용 전 충분한 테스트 권장
  • 점진적 도입 전략 수립 필요

실제 성능 데이터

Meta의 사례

Meta Quest Store 프로덕션 적용 결과:

  • 초기 로드 및 페이지 전환: 최대 12% 향상
  • 특정 인터랙션: 2.5배 이상 빠름
  • 메모리 사용량: 중립 유지

Instagram의 경험

React Compiler는 이미 instagram.com 프로덕션에서 사용 중이며, Meta 내부의 여러 대규모 애플리케이션에서 검증되었습니다.

Vercel의 도입

Next.js 15.3+ 버전에서:

  • 개발 세션의 50% 이상이 Turbopack 사용
  • 프로덕션 빌드의 20%가 Turbopack 사용
  • vercel.com, nextjs.org 등에서 12억+ 요청 처리

메모이제이션의 실제 비용

과도한 메모이제이션의 문제

React 팀이 Meta에서 수행한 연구 결과:

1수동 메모이제이션을 사용한 PR
2→ 작성 시간 31-46% 증가
3→ 전체 PR의 단 8%만 사용
4
5결론: 수동 메모이제이션은 상당한 인지 부담

실제 코드베이스에서의 문제

1const Component = memo(({ data }) => {
2  const a = useMemo(() => data.x, [data.x]);
3  const b = useMemo(() => data.y, [data.y]);
4  const c = useCallback(() => console.log(a), [a]);
5  const d = useMemo(() => a + b, [a, b]);
6  const e = useCallback(() => c(), [c]);
7  const f = useMemo(() => d * 2, [d]);
8
9  return <div>{f}</div>;
10});
11

이 최적화로 절약한 시간: 약 0.3ms

발생하는 문제:

  • 코드 복잡도 증가
  • 리뷰 시간 증가
  • 실제 성능 향상: 미미하거나 없음
  • 버그 가능성 증가

실용적인 가이드라인

언제 메모이제이션이 필요한가?

React 공식 문서 기준: 총 계산 시간이 1ms 이상일 때

정말 비싼 계산

1const expensiveResult = useMemo(() => {
2  console.time('expensive');
3  const result = hugeArray
4    .filter(item => item.active)
5    .sort((a, b) => b.score - a.score)
6    .slice(0, 100)
7    .map(item => complexTransform(item));
8  console.timeEnd('expensive');
9  return result;
10}, [hugeArray]);

memo 컴포넌트에 함수 전달

1const MemoizedChild = memo(Child);
2
3function Parent() {
4  const handler = useCallback(() => {
5    doSomething();
6  }, []);
7
8  return <MemoizedChild onClick={handler} />;
9}

Context 값 최적화

1const AuthContext = React.createContext({});
2
3function AuthProvider({ user, children }) {
4  const value = useMemo(() => ({
5    user,
6    login,
7    logout,
8  }), [user]);
9
10  return (
11    <AuthContext.Provider value={value}>
12      {children}
13    </AuthContext.Provider>
14  );
15}

언제 메모이제이션이 불필요한가?

DOM 요소에 직접 전달

<button onClick={useCallback(() => {}, [])}>
  Click
</button>

자식이 일반 DOM 요소라면 useCallback은 불필요합니다.

자식이 memo가 아닌 경우

<RegularChild onClick={useCallback(() => {}, [])} />

RegularChild가 memo로 감싸져 있지 않다면 무의미합니다.

간단한 계산

const sum = useMemo(() => a + b, [a, b]);

그냥 const sum = a + b;로 충분합니다.

항상 변하는 값

const obj = useMemo(() => ({
  timestamp: Date.now()
}), []);

의존성이 없어도 매번 새로운 값이면 의미가 없습니다.

권장하는 워크플로우

  1. 깔끔한 코드 작성 (메모이제이션 없이)
  2. 개발 및 테스트
  3. 성능 문제 발견 시
  4. React DevTools Profiler로 병목 확인
  5. 구조적 개선 시도 (상태 위치, 컴포넌트 분리 등)
  6. 여전히 느리다면 진짜 병목에만 메모이제이션 추가
  7. 성능 재측정

Best Practice

새 프로젝트

React Compiler를 활성화한 후에는 깔끔하게 작성하면 됩니다.

1function ProductList({ products, onSelect }) {
2  const filtered = products.filter(p => p.inStock);
3
4  return (
5    <div>
6      {filtered.map(product => (
7        <ProductCard
8          key={product.id}
9          product={product}
10          onSelect={() => onSelect(product.id)}
11        />
12      ))}
13    </div>
14  );
15}

컴파일러가 알아서 최적화합니다.

기존 프로젝트

점진적 도입을 고려해보세요.

  1. 기존 메모이제이션은 유지 (제거하면 컴파일 출력 변경 가능)
  2. 새로운 코드에는 메모이제이션 추가하지 않기
  3. React Compiler 점진적 도입
  4. 안정화 후 기존 메모이제이션 제거 고려

재사용 가능한 라이브러리와 훅

라이브러리 코드는 여전히 미리 최적화하는 것이 좋습니다.

1export function useToggle(initialValue = false) {
2  const [value, setValue] = useState(initialValue);
3
4  const toggle = useCallback(() => {
5    setValue(v => !v);
6  }, []);
7
8  const setTrue = useCallback(() => setValue(true), []);
9  const setFalse = useCallback(() => setValue(false), []);
10
11  return [value, toggle, setTrue, setFalse] as const;
12}
13

어디서 사용될지 모르기 때문에 최적화가 필요합니다.


현실적인 고려사항

React Compiler의 한계

컴포넌트와 훅이 아닌 일반 함수는 메모이제이션되지 않습니다.

function expensiveUtility(data) {
  return data.map(heavyCalculation);
}

정말 비싼 계산은 여전히 수동 최적화가 필요할 수 있습니다.

1const cache = new Map();
2
3function expensiveUtility(data) {
4  const key = JSON.stringify(data);
5  if (cache.has(key)) return cache.get(key);
6
7  const result = data.map(heavyCalculation);
8  cache.set(key, result);
9  return result;
10}

메모이제이션은 컴포넌트 간 공유되지 않음

여러 컴포넌트에서 같은 비싼 계산을 사용한다면 각 컴포넌트마다 별도로 계산됩니다. 이런 경우 전역 캐시나 상태 관리가 더 적합할 수 있습니다.


결론

현재 상황 (2025년 10월)

  • React Compiler: Next.js 16에서 안정화
  • 프로덕션 검증: Meta, Instagram 등 대규모 서비스에서 사용 중
  • 완전한 대체: 아직 몇 년은 걸릴 것
  • 기존 지식: 여전히 중요 (레거시, 마이그레이션, 특수 케이스)

권장하는 접근

신규 프로젝트

  • React Compiler 활성화 고려
  • 메모이제이션 최소화
  • 성능 문제 시에만 추가

기존 프로젝트

  • 과도한 메모이제이션 리팩토링 고려
  • "Profile First, Optimize Second"
  • React Compiler 점진적 도입 계획

라이브러리/재사용 코드

  • 여전히 선제적 최적화 권장
  • API 안정성 중요

마지막 조언

"조기 최적화는 모든 악의 근원" - Donald Knuth

좋은 코드의 우선순위:

  1. 읽기 쉽고 이해하기 쉬운 코드
  2. 유지보수하기 쉬운 구조
  3. 올바른 동작
  4. 측정 가능한 성능 최적화 (필요시)

React Compiler는 강력한 도구이지만, 깔끔하고 이해하기 쉬운 코드를 대체할 수는 없습니다. 도구는 도구일 뿐, 좋은 설계와 명확한 코드가 먼저라고 생각합니다.


참고 자료


이 글이 도움되셨나요?

공유해주시면 더 많은 사람들이 볼 수 있어요!