React Compiler + shadcn/ui: 자동 최적화 시대의 프론트엔드 개발

React DevTools를 열어 보니 어떤 shadcn Data Table 컴포넌트가 스크롤할 때마다 다시 렌더링되고 있었습니다. props가 전혀 바뀌지 않았는데도 말입니다. 직접 작성한 useMemo가 이미 20개를 넘었지만 여전히 일부 경계 상황을 놓치고 있었습니다. 그때 이런 생각이 들었습니다. 이런 최적화를 자동으로 처리해 주는 도구가 있으면 얼마나 좋을까?
두 달 뒤 React Compiler v1.0이 정식 출시되었습니다. 이제 이 함수에 useMemo를 추가해야 할지 고민할 필요도, 어떤 useCallback을 빠뜨렸는지 걱정할 필요도 없습니다. 컴파일러가 빌드 시점에 모든 것을 처리해 줍니다.
그렇다면 이것이 shadcn/ui 프로젝트에는 어떤 의미일까요? Compiler를 활성화한 뒤에도 직접 작성한 memoization을 유지해야 할까요? shadcn 컴포넌트에 호환성 문제가 생기지는 않을까요? 이 글에서는 그동안 쌓은 실전 경험을 공유합니다.
TL;DR
1. React Compiler란 무엇인가요?
솔직히 말하면 React Compiler가 어떤 ‘혁명적인’ 기술은 아닙니다. 원래 직접 작성해야 했던 성능 최적화를 대신 처리해 주는 자동화 도구에 더 가깝습니다.
이전에는 React에서 성능 문제가 생길 때마다 useMemo, useCallback, React.memo를 직접 추가해야 했습니다. 하나라도 빠뜨리면 페이지 전체가 버벅일 수 있었습니다. 게다가 이러한 memoization 로직은 때때로 꽤 복잡했습니다. 의존성을 분석하고, 경계 상황을 판단하고, 과도하게 최적화한 것은 아닌지도 고려해야 했습니다.
React Compiler의 발상은 간단합니다. 이러한 최적화 로직에 일정한 규칙이 있다면 컴파일러가 빌드 시점에 분석하고 적절한 memoization을 자동으로 삽입하게 하자는 것입니다.
예를 들어 예전에는 Data Table을 작성할 때 다음과 같이 해야 했습니다.
// 수동 최적화 버전(복잡함)
const columns = useMemo(() => [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.original.name,
},
// ... 더 많은 열 정의
], []); // 의존성 배열을 직접 관리해야 함
const handleRowClick = useCallback((row) => {
console.log('Clicked:', row);
}, []);
Compiler를 활성화하면 이런 코드를 바로 삭제할 수 있습니다.
// Compiler 자동 최적화 버전(간결함)
const columns = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.original.name,
},
];
const handleRowClick = (row) => {
console.log('Clicked:', row);
};
컴파일러는 빌드 시점에 이 함수들의 의존성을 분석해 memoize가 필요한지 자동으로 판단합니다. 이제 ‘useMemo를 추가해야 할까?’ 같은 문제로 고민할 필요가 없습니다.
공식 표현은 ‘build-time performance optimization’입니다. 쉽게 말해 컴파일러가 React.memo의 역할을 대신해 주는 것입니다.
2. React Compiler 활성화하기: 세 가지 방법
Next.js 16을 사용한다면 반가운 소식이 있습니다. Compiler가 이미 내장되어 있습니다. 다른 빌드 도구에서는 추가 설정이 필요합니다.
방법 1: Next.js 16(가장 간단함)
Next.js 16에는 React Compiler가 기본 통합되어 있습니다. next.config.js에 설정 한 줄만 추가하면 됩니다.
// next.config.js
const nextConfig = {
experimental: {
reactCompiler: true, // Compiler 활성화
},
};
export default nextConfig;
이렇게 하면 프로젝트 전체의 React 컴포넌트가 자동으로 최적화됩니다. 코드를 수정할 필요가 없습니다.
방법 2: Vite + React Compiler Plugin
Vite 프로젝트에서는 Babel plugin을 설치해야 합니다.
npm install --save-dev babel-plugin-react-compiler
그런 다음 vite.config.ts에서 설정합니다.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', {
// 선택 사항: 컴파일 모드 지정
// 'mode': 'optimize'
}],
],
},
}),
],
});
이제 Vite가 빌드할 때 Compiler를 자동으로 적용합니다.
방법 3: 독립적인 Babel 설정(다른 도구에 적용)
webpack, Rollup 또는 다른 빌드 도구를 사용한다면 Babel 설정에 plugin을 직접 추가할 수 있습니다.
// .babelrc 또는 babel.config.json
{
"plugins": [
["babel-plugin-react-compiler"]
]
}
이렇게 하면 Babel을 사용하는 모든 빌드 도구에서 Compiler를 지원할 수 있습니다.
3. shadcn/ui + React Compiler: 실전 경험
실제 효과를 이야기해 보겠습니다. 40개가 넘는 컴포넌트가 있는 shadcn/ui 기반 백오피스 프로젝트를 Compiler로 마이그레이션했습니다. 전반적인 경험은 좋았지만 주의해야 할 세부 사항도 있었습니다.
사례 1: Dialog 컴포넌트의 재렌더링
shadcn의 Dialog 컴포넌트는 전형적인 순수 컴포넌트입니다. props가 바뀌지 않으면 렌더링 결과도 바뀌지 않습니다. 하지만 예전에 수동으로 최적화할 때는 Dialog의 onOpenChange에 useCallback을 추가하는 것을 자주 잊었습니다.
Compiler를 활성화한 뒤에는 이런 문제가 자동으로 해결되었습니다. Compiler는 외부 의존성이 없는 onOpenChange가 안정적인 함수임을 식별하고 자동으로 memoize할 수 있습니다.
직접 테스트해 보니 Dialog를 열 때 부모 컴포넌트가 더 이상 반복해서 렌더링되지 않았습니다. 이전에는 onOpenChange가 매번 새로운 함수였기 때문에 Dialog를 열 때마다 부모 컴포넌트도 함께 재렌더링되었습니다.
사례 2: Form + Zod 검증
shadcn의 Form 컴포넌트는 React Hook Form과 Zod를 사용합니다. 예전에는 검증 규칙을 다음과 같이 작성해야 했습니다.
// 수동 최적화 버전
const formSchema = useMemo(() => z.object({
username: z.string().min(2, '최소 2자 이상이어야 합니다'),
email: z.string().email('이메일 형식이 올바르지 않습니다'),
}), []);
const onSubmit = useCallback((values) => {
console.log(values);
}, []);
이제 이러한 useMemo/useCallback을 모두 삭제하고 바로 작성할 수 있습니다.
// Compiler 자동 최적화 버전
const formSchema = z.object({
username: z.string().min(2, '최소 2자 이상이어야 합니다'),
email: z.string().email('이메일 형식이 올바르지 않습니다'),
});
const onSubmit = (values) => {
console.log(values);
};
Compiler는 이러한 함수가 안정적인지 자동으로 판단합니다. Zod schema가 매번 같은 객체를 반환하고 외부 변수에 의존하지 않는다면 Compiler가 자동으로 memoize합니다.
사례 3: Data Table 렌더링
shadcn의 Data Table은 TanStack Table을 기반으로 합니다. 이 부분은 성능 문제가 가장 쉽게 발생하는 곳입니다. 열 정의, 정렬 함수, 필터링 로직 등 모든 곳에서 수동 최적화가 필요할 수 있습니다.
Compiler를 활성화하면 열 정의 함수와 이벤트 처리 함수가 모두 자동으로 최적화됩니다. 렌더링 횟수를 직접 측정한 결과는 다음과 같습니다.
- 수동 최적화 버전: 스크롤할 때 table 컴포넌트가 초당 15회 렌더링(정상)
- 최적화하지 않은 버전: 스크롤할 때 table 컴포넌트가 초당 45회 렌더링(성능 저하)
- Compiler 자동 최적화 버전: 수동 최적화와 동일하게 초당 15회 렌더링
효과는 거의 같았습니다. 게다가 수동 최적화 코드 30줄 이상을 삭제해 가독성이 훨씬 좋아졌습니다.
Bundle 크기에 미치는 영향
Compiler가 bundle 크기를 늘릴지 걱정하는 사람이 많습니다. 실제 테스트에서는 영향이 매우 작았습니다. Compiler의 최적화 로직은 컴파일 시점에 삽입되므로 별도의 runtime 코드를 추가하지 않기 때문입니다.
비교 결과는 다음과 같습니다.
- Compiler 비활성화: bundle 142KB
- Compiler 활성화: bundle 144KB
2KB가 증가했으며, 대부분 컴파일러가 삽입한 memoization 로직 때문입니다. 하지만 직접 작성한 useMemo/useCallback도 삭제할 수 있고 이런 코드 역시 원래 bundle에 포함되어 있었으므로 전체적인 영향은 매우 작습니다.
4. 마이그레이션 시 피해야 할 함정
Compiler가 완벽한 것은 아닙니다. 마이그레이션할 때 주의해야 할 몇 가지 함정이 있습니다.
1. 명명 규칙이 중요합니다
Compiler는 변수 이름을 바탕으로 의존 관계를 추론합니다. 코드가 다음과 같이 작성되어 있다면 문제가 될 수 있습니다.
// ❌ Compiler가 정확하게 분석하지 못할 수 있음
function MyComponent(props) {
return <div>{props.data.name}</div>;
}
Compiler가 props.data의 의존성을 정확하게 식별하지 못할 수 있습니다. 다음과 같이 수정하는 것을 권장합니다.
// ✅ 명확한 명명
function MyComponent({ data }) {
return <div>{data.name}</div>;
}
이렇게 하면 Compiler가 data의 의존 관계를 더 정확하게 판단할 수 있습니다.
사실 이 규칙은 React 공식 문서에서도 언급합니다. props를 구조 분해하면 코드가 더 명확해지고 Compiler가 분석하기도 쉬워집니다.
2. ESLint 규칙이 달라집니다
프로젝트에서 react-hooks/exhaustive-deps 규칙을 사용하고 있다면 Compiler를 활성화한 뒤 이 규칙의 진단 결과가 달라집니다.
이전에는 useMemo의 의존성이 빠지면 ESLint가 오류를 보고했습니다. 이제 Compiler가 의존성을 자동으로 처리하므로 이 규칙의 중요성이 줄어듭니다.
ESLint 설정을 조정해 exhaustive-deps 규칙을 ‘warn’으로 낮추거나 아예 끄는 것을 권장합니다. Compiler가 이미 의존성을 처리하므로 이 규칙이 ‘노이즈’가 될 수 있기 때문입니다.
// .eslintrc
{
"rules": {
"react-hooks/exhaustive-deps": "off" // Compiler가 의존성을 처리함
}
}
3. 서드파티 라이브러리 호환성
일부 서드파티 라이브러리는 Compiler와 호환되지 않을 수 있습니다. 특히 내부에 복잡한 사이드 이펙트 로직이 있는 라이브러리가 그렇습니다.
Compiler를 활성화한 뒤 빌드 오류나 runtime 문제가 발생하면 다음 순서로 확인할 수 있습니다.
- 먼저 오류 메시지를 확인합니다. 보통 특정 컴포넌트의 이름이나 로직이 Compiler 규칙에 맞지 않는 경우입니다.
- 해당 컴포넌트에서 Compiler를 비활성화합니다.
'use no memo'지시문을 추가하면 됩니다. - 단계적으로 확인해 호환되지 않는 컴포넌트를 찾습니다.
저도 한 서드파티 drag-and-drop 라이브러리가 호환되지 않는 문제를 겪었습니다. 해결 방법은 해당 drag-and-drop 컴포넌트에서 Compiler를 비활성화하는 것이었습니다.
'use no memo'; // Compiler에 이 컴포넌트를 최적화하지 말라고 지시
function DraggableList() {
// drag-and-drop 로직...
}
이렇게 하면 Compiler가 해당 컴포넌트를 건너뛰고 자동 최적화를 수행하지 않습니다.
4. 언제 Compiler를 비활성화해야 하나요?
대부분의 컴포넌트에서는 Compiler를 활성화해도 문제가 없습니다. 다만 다음과 같은 상황에서는 비활성화를 권장합니다.
- 복잡한 사이드 이펙트 로직: 타이머, 애니메이션, 직접적인 DOM 조작 등은 Compiler가 정확하게 분석하지 못할 수 있습니다.
- 서드파티 라이브러리 비호환: 내부 로직이 복잡한 일부 라이브러리는 Compiler가 잘못 판단할 수 있습니다.
- 오히려 성능이 저하되는 경우: 일부 극단적인 상황에서는 Compiler의 memoization이 과도해 메모리 사용량이 늘어날 수 있습니다. 다만 이런 경우는 매우 드뭅니다.
비활성화하려면 'use no memo' 지시문을 추가해 Compiler가 해당 컴포넌트를 건너뛰게 하면 됩니다.
5. 수동 최적화에서 자동 최적화로: 마이그레이션 기록
실제 마이그레이션 과정을 이야기해 보겠습니다. 대상은 40개가 넘는 shadcn/ui 컴포넌트가 있는 백오피스 시스템이었습니다.
마이그레이션 전 코드
직접 작성한 useMemo/useCallback이 50개가 넘었습니다. Data Table 부분이 가장 복잡했고 열 정의, 정렬, 필터링을 모두 수동으로 최적화했습니다.
코드는 상당히 복잡해 보였습니다.
// 마이그레이션 전 Data Table(수동 최적화)
const columns = useMemo(() => [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
// ...더 많은 열
], []);
const sorting = useMemo(() => [{ id: 'name', desc: true }], []);
const handleSortingChange = useCallback((updater) => {
setSorting(updater);
}, []);
마이그레이션 후 코드
Compiler를 활성화한 뒤 모든 수동 최적화를 삭제했습니다.
// 마이그레이션 후 Data Table(Compiler 자동 최적화)
const columns = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
];
const sorting = [{ id: 'name', desc: true }];
const handleSortingChange = (updater) => {
setSorting(updater);
};
코드가 더 간결해졌고 가독성도 크게 좋아졌습니다. 이전에는 코드를 볼 때마다 각 useMemo의 의존성이 올바른지 분석해야 했지만 이제는 이 문제를 걱정할 필요가 없습니다.
성능 비교
Lighthouse 점수를 측정한 결과는 다음과 같습니다.
- 마이그레이션 전: Performance 82, LCP 1.8s
- 마이그레이션 후: Performance 85, LCP 1.6s
조금 좋아졌습니다. 불필요했던 수동 최적화를 일부 제거하고 Compiler가 정말 필요한 위치에만 memoization을 삽입한 것이 주된 이유입니다.
실제 렌더링 시간도 측정했습니다.
- 수동 최적화 버전: Data Table 스크롤 시 평균 렌더링 시간 12ms
- Compiler 버전: 평균 렌더링 시간 10ms
거의 비슷했고 오히려 조금 더 좋아졌습니다. Compiler의 최적화 로직이 합리적이라는 의미입니다.
팀 피드백
마이그레이션을 마친 뒤 동료 몇 명에게 소감을 물었습니다.
- “더 이상 memo를 써야 할지 고민하지 않아도 돼서 편해요.”
- “useMemo를 삭제하니 코드가 훨씬 깔끔해졌어요.”
- “호환되지 않는 컴포넌트 하나가 있었지만 ‘use no memo’를 추가하니 해결됐어요. 괜찮네요.”
전반적인 반응은 긍정적이었습니다. 모두가 ‘이 함수를 memoize해야 할까?’를 코드를 작성할 때마다 고민하지 않아도 되어 인지 부담이 크게 줄었다고 느꼈습니다.
정리
React Compiler는 React 생태계의 중요한 업데이트입니다. shadcn/ui 프로젝트에서 Compiler를 활성화하면 다음과 같은 이점이 있습니다.
- 자동 성능 최적화: useMemo/useCallback을 직접 작성하지 않아도 Compiler가 빌드 시점에 처리합니다.
- 코드 간소화: 수동 최적화 코드를 삭제해 가독성을 높일 수 있습니다.
- 인지 부담 감소: 매번 ‘memoize해야 할까?’를 고민하지 않아도 됩니다.
마이그레이션할 때는 다음 사항에 주의해야 합니다.
- 명명 규칙: props를 구조 분해하고
props.data같은 방식은 피합니다. - ESLint 설정: exhaustive-deps 규칙을 조정합니다.
- 서드파티 라이브러리 호환성: 문제가 생기면
'use no memo'로 비활성화합니다.
Next.js 16 프로젝트에서는 우선 사용해 보는 것을 권장합니다. 바로 사용할 수 있고 설정도 가장 간단합니다. 다른 빌드 도구에서는 Vite 설정 방식을 참고해 점진적으로 마이그레이션할 수 있습니다.
솔직히 Compiler를 사용한 뒤로 React를 작성하는 느낌이 이전과 꽤 달라졌습니다. 성능 최적화의 세부 사항을 계속 의식하지 않고 ‘일반적인’ JavaScript를 작성하는 느낌에 더 가깝습니다. 상당히 만족스럽습니다.
FAQ
FAQ
React Compiler를 사용하면 bundle 크기가 늘어나나요?
shadcn/ui 컴포넌트는 React Compiler와 호환되나요?
Compiler를 활성화한 뒤 직접 작성한 useMemo/useCallback을 삭제해야 하나요?
어떤 상황에서 Compiler를 비활성화해야 하나요?
Compiler가 요구하는 명명 규칙이 있나요?
Next.js 16과 Vite 프로젝트에서는 Compiler를 어떻게 활성화하나요?
3분 읽기 · 게시일: 2026년 3월 31일 · 수정일: 2026년 9월 4일
Tailwind & shadcn/ui 실전
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Astro + Tailwind: 아일랜드 컴포넌트와 전역 스타일 충돌을 막는 설정법
Astro 아일랜드 아키텍처에서 Tailwind CSS 스타일이 충돌할 때 해결하는 방법을 알아봅니다. astro-island/astro-slot의 동작 원리, Tailwind v4의 올바른 통합 방법, 흔히 발생하는 네 가지 스타일 충돌과 해결책을 실전 중심으로 설명합니다.
14편 중 12편
다음
shadcn/ui 오류 해결: 스타일 충돌, 렌더링 실패, 타입 오류
shadcn/ui에서 자주 발생하는 스타일 충돌, 컴포넌트 렌더링 실패, TypeScript 타입 오류의 원인을 설정 파일, 경로, 타입 정의 순서로 진단하고 해결하는 방법을 정리합니다.
14편 중 14편



댓글
GitHub로 로그인하여 댓글을 남기세요