코드 리뷰 및 최적화
북스토어 코드를 책임·가독성·안전성 기준으로 리뷰하고 이미지·폰트·데이터·번들 성능을 측정해 개선합니다.
실전 프로젝트를 마무리할 때 코드 리뷰와 최적화는 반드시 거쳐야 하는 단계입니다.
기능이 동작하는 수준을 넘어 코드를 더 효율적이고 유지보수하기 쉽게 만들고, 성능까지 끌어올리는 데 직접 기여하기 때문입니다.
이 절에서는 코드 리뷰의 핵심 관점과 일반적인 최적화 기법, 성능 개선 도구 활용법을 다룹니다.
코드 리뷰의 중요성 및 방법
코드 리뷰는 변경된 코드의 정확성, 유지보수성, 성능 영향, 테스트 범위를 검토하는 과정입니다.
리뷰에서는 단순한 스타일보다 요구사항 충족 여부와 회귀 위험을 먼저 확인합니다.
북스토어의 변경을 한 가지 주장으로 좁히기 예를 들어 도서 목록 이미지의 다운로드가 큰 문제라면 이미지의 표시 폭·요청 크기부터 확인합니다. 검색 버튼 반응이 느리다면 같은 검색 동작의 실행 기록과 요청 시간을 봅니다. 코드 줄 수가 줄거나 린트가 통과했다는 사실만으로 두 문제가 해결된 것은 아닙니다. 기능 조건도 함께 지킵니다. 장바구니 버튼을 숨겨 렌더링을 줄여도 서버의 주문 권한 검사를 생략하면 올바른 최적화가 아닙니다.
코드 리뷰의 이점
- 버그 조기 발견: 개발 초기 단계에서 논리적 오류나 잠재적 버그를 발견하여 수정 비용을 절감합니다.
- 코드 품질 향상: 가독성, 유지보수성, 재사용성 측면에서 더 나은 코드를 작성하도록 유도합니다.
- 지식 공유 및 팀 역량 강화: 팀원 간에 기술과 모범 사례를 공유하고, 서로의 강점과 약점을 파악하여 학습 기회를 제공합니다.
- 일관성 유지: 코딩 스타일, 아키텍처 패턴 등 프로젝트 전반의 일관성을 유지하는 데 도움이 됩니다.
- 보안 취약점 발견: 잠재적인 보안 취약점을 식별하고 개선합니다.
효과적인 코드 리뷰 방법
- 명확한 목표 설정: 무엇을 중점적으로 볼 것인지(예: 기능 정확성, 성능, 보안, 가독성) 미리 정합니다.
- 작은 단위로 리뷰: 한 번에 너무 많은 코드를 리뷰하지 않도록 합니다. PR(Pull Request) 크기를 작게 유지하는 것이 좋습니다.
- 긍정적인 분위기 조성: 비판보다는 건설적인 피드백에 집중하고, 존중하는 태도로 의견을 교환합니다.
- 체크리스트 활용: 일반적인 코딩 표준, 보안 고려사항, 성능 가이드라인 등을 포함하는 체크리스트를 활용하여 누락 없이 검토합니다.
- 자동화 도구 활용: ESLint, Prettier와 같은 린터(Linter)와 포맷터(Formatter)를 사용하여 기본적인 코드 스타일과 잠재적 오류를 자동으로 검사합니다. 이는 수동 리뷰의 부담을 줄여 줍니다.
- 테스트 코드 확인: 관련 테스트 코드가 충분한지, 그리고 테스트가 통과하는지 확인합니다.
Next.js 성능 최적화 기법
Next.js는 기본적으로 빠른 성능을 제공하지만, 복잡한 애플리케이션에서는 추가적인 최적화가 필요합니다.
이미지 최적화 (next/image)
next/image는 이미지 최적화와 크기별 제공, 기본 지연 로딩을 지원합니다. 효과는 이미지·설정·캐시·네트워크에 따라 측정합니다. 아래 preload는 첫 화면의 주요 이미지에 한정한 선택이며 모든 도서 카드에 붙이지 않습니다.
- 적용 방법: 일반
<img>태그 대신next/image를 사용하고src와 의미에 맞는alt를 지정합니다. 이미지 공간은 정적 import가 추론한 크기,width와height, 또는fill중 하나로 확보합니다. -
예시
import Image from 'next/image'; function BookImage({ src, alt }: { src: string; alt: string }) { return ( <Image src={src} alt={alt} width={500} height={600} sizes="(max-width: 768px) 100vw, 500px" style={{ width: '100%', height: 'auto' }} placeholder="blur" blurDataURL="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" preload /> ); }
이미지의 선언 크기와 실제 표시 폭
위 width·height는 500:600의 비율을 알려 공간을 확보하고, CSS는 부모 폭에 맞춰 표시합니다. sizes는 표시 폭을 브라우저에 알려 srcset 후보를 고르는 힌트이며 이미지 CSS 폭을 직접 설정하지 않습니다. 작은 화면에서 실제 카드가 viewport 전체보다 좁다면 100vw를 무조건 쓰기보다 실제 레이아웃에 맞는 힌트를 작성합니다. preload도 모든 이미지에 적용하면 같은 네트워크 자원을 경쟁하므로 첫 화면의 주요 이미지에 필요한지 측정합니다. 예제의 1픽셀 blurDataURL은 자리 표시 동작을 보여 주는 값이며 실제 책 표지의 축소 미리보기는 아닙니다.
폰트 최적화 (next/font)
next/font는 글꼴 파일을 애플리케이션에서 제공하고 폰트 메트릭 조정으로 글꼴 교체 시 레이아웃 이동을 줄일 수 있습니다. next/font/google은 빌드 때 파일을 가져오므로 브라우저가 Google에 직접 요청하지 않지만, 자체 호스팅된 폰트 다운로드는 여전히 발생합니다. 모든 원인의 CLS가 없어지는 것은 아닙니다.
- 적용 방법:
next/font/google또는next/font/local에서 폰트를 임포트하여 사용합니다. -
예시
app/layout.tsx import { Inter } from 'next/font/google'; import type { ReactNode } from 'react'; const inter = Inter({ subsets: ['latin'], display: 'swap' }); export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="ko" className={inter.className}> <body>{children}</body> </html> ); }
스크립트 최적화 (next/script)
서드파티 스크립트(Google Analytics, 광고 스크립트 등)는 페이지 로딩 성능에 큰 영향을 미칠 수 있습니다.
next/script 컴포넌트를 사용하면 스크립트 로딩 전략을 제어할 수 있습니다.
-
전략
beforeInteractive: hydration 전에 필요한 중요 스크립트로, Root Layout에 배치afterInteractive(기본값): 페이지의 일부 hydration이 진행된 뒤 로드lazyOnload: 브라우저가 유휴 상태일 때 낮은 우선순위로 로드worker: 웹 워커에서 로드하는 실험적 전략이며 App Router에서는 지원하지 않음
-
예시
import Script from 'next/script'; function MyAnalytics() { return ( <> <Script src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID" strategy="afterInteractive" /> <Script id="google-analytics" strategy="afterInteractive"> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'GA_MEASUREMENT_ID'); `} </Script> </> ); }
데이터 페칭 최적화 (캐싱 및 재검증)
Next.js의 캐싱 메커니즘을 이해하고 효율적으로 활용합니다.
- Data Cache: Next.js 15 이후
fetch응답은 기본적으로 캐시되지 않습니다. 반복해서 재사용할 데이터는cache: 'force-cache'로 명시해 캐싱합니다. Next.js 16에서cacheComponents: true를 활성화한 프로젝트라면use cache지시어도 사용할 수 있습니다. - Full Route Cache: 정적으로 렌더링한 라우트의 HTML과 React Server Component payload를 서버에 저장합니다. 개발 모드에서는 페이지를 요청 시점에 렌더링하므로 운영 캐시 동작과 구분해야 합니다.
- Incremental Static Regeneration (ISR): 정적으로 생성된 페이지는 유효기간이 지난 뒤 다음 요청을 계기로 재검증하거나, 명시적인 on-demand 재검증으로 업데이트할 수 있습니다. 이는 빌드 시간을 단축하면서도 최신 콘텐츠를 제공하는 데 유용합니다.
- App Router에서 ISR:
fetch의revalidate옵션을 사용합니다. 필요할 때 경로는revalidatePath, 태그의 SWR 갱신은revalidateTag(tag, 'max'), Server Action의 즉시 만료는updateTag로 처리합니다. - 예시: 아래 세그먼트 설정은
cacheComponents를 사용하지 않는 이전 캐시 모델용입니다.[id]경로를 ISR로 처리하려면generateStaticParams로 정적 경로를 준비하거나 빈 배열을 반환하는 등 정적 생성 조건도 함께 갖춰야 합니다. 17장 2절의 주석 처리된generateStaticParams를 그대로 둔 채 이 한 줄만 추가해 모든 요청이 캐시된다고 보장하지 않습니다.app/books/[id]/page.tsx export const revalidate = 60; // 60초가 지난 뒤 다음 요청을 계기로 재검증
- App Router에서 ISR:
- Incremental Static Regeneration (ISR): 정적으로 생성된 페이지는 유효기간이 지난 뒤 다음 요청을 계기로 재검증하거나, 명시적인 on-demand 재검증으로 업데이트할 수 있습니다. 이는 빌드 시간을 단축하면서도 최신 콘텐츠를 제공하는 데 유용합니다.
- Static Assets Cache:
/_next/static아래의 해시 기반 빌드 자산은 장기 캐시에 적합합니다.public파일은 같은 이름으로 교체될 수 있으므로 필요한 캐시 헤더를 배포 환경에서 별도로 정합니다.
ISR을 요청 시간으로 추적하기
다음은 cacheComponents를 쓰지 않고 정적 생성 조건을 갖춘 운영 라우트의 설명입니다. 시각 0초에 만든 페이지가 있고 DB 값은 20초에 바뀌었다고 합시다. revalidate = 60만으로 60초 정각에 새 페이지를 만드는 타이머가 생기지는 않습니다. 만료 뒤 첫 요청이 75초에 오면 기존 페이지를 먼저 제공하면서 재생성을 시작할 수 있습니다. 재생성이 성공한 뒤의 요청은 새 페이지를 받으며, 실패하면 마지막으로 성공한 페이지를 유지하고 이후 요청에서 재시도합니다. 재검증 전에 재고가 꼭 최신이어야 하는 주문 검사는 이 캐시된 화면 값만 믿지 않고 서버의 실제 업무 조건을 다시 확인합니다.
페이지 단위 분할과 지연 로딩 설계
Next.js는 페이지 단위 코드 스플리팅을 자동으로 처리합니다.
공유 레이아웃·공통 모듈과 사전 가져오기 경로도 함께 고려해야 하므로, 네트워크 요청이 현재 페이지의 고유 코드에만 한정된다는 뜻은 아닙니다.
추가적으로, 특정 컴포넌트나 라이브러리를 필요할 때만 로드하도록 동적 임포트를 사용할 수 있습니다.
next/dynamic: 클라이언트 컴포넌트나 무거운 라이브러리를 레이지 로드하는 데 사용합니다.-
예시
src/components/DashboardChart.tsx (클라이언트 컴포넌트) "use client"; // 무거운 차트 라이브러리를 동적으로 로드 import dynamic from 'next/dynamic'; import Loading from './Loading'; const DynamicChart = dynamic(() => import('../components/Chart'), { loading: () => <Loading />, // 로딩 중 표시할 컴포넌트 ssr: false, // 이 컴포넌트는 클라이언트에서만 렌더링 (SSR 제외) }); export default function MyDashboard() { return ( <div> <DynamicChart /> </div> ); }
SSR 제외와 사용 시점까지 미루기는 다릅니다
위 MyDashboard는 처음부터 DynamicChart를 렌더링합니다. ssr: false는 서버 사전 렌더링을 제외하는 선택이며 클릭 조건을 만들지는 않습니다. 차트를 열 때만 렌더링하려면 다음처럼 별도의 표시 조건을 둡니다. Chart와 Loading은 앞 예제처럼 프로젝트에 준비돼 있다는 전제입니다.
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
import Loading from './Loading';
const DynamicChart = dynamic(() => import('./Chart'), {
loading: () => <Loading />,
ssr: false,
});
export default function OnDemandDashboard() {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen(true)}>차트 열기</button>
{open && <DynamicChart />}
</section>
);
}여기서 첫 렌더는 버튼만 포함하고, open이 true인 렌더부터 차트가 포함됩니다. 코드 분할·다운로드·차트 실행 시간의 실제 변화는 빌드와 Network·Performance 기록으로 확인합니다. 한 조건이 false인 것만으로 다른 공통 모듈·사전 가져오기·네트워크 비용 전체가 사라졌다고 단정하지 않습니다.
성능 측정 및 디버깅 도구
Next · Performance evidence scopes
성능 질문에 맞는 측정 근거
첫 화면 진단, 실행 원인, 실제 사용자 분포, 번들 크기는 서로 다른 근거입니다. 한 도구의 점수나 크기만으로 북스토어의 전체 성능과 원인을 확정하지 않습니다.
| 대상 | 먼저 볼 근거 | 이것만으로 알 수 없는 것 |
|---|---|---|
| 첫 화면 지연 | Lighthouse의 실험실 LCP·CLS·TBT | 실제 사용자의 모든 상호작용 지연 |
| 실행 원인 | DevTools Performance의 작업·렌더링 기록 | 전체 사용자 집단의 분포 |
| 실사용 환경 | Speed Insights 등의 실제 사용자 지표 | 느린 함수의 구체적인 실행 원인 |
| 번들 크기 | 사용한 번들러의 모듈별 분석 | 모듈 크기만으로 실제 실행 시간 |
- 대상: 첫 화면 지연
- 먼저 볼 근거
- Lighthouse의 실험실 LCP·CLS·TBT
- 이것만으로 알 수 없는 것
- 실제 사용자의 모든 상호작용 지연
- 대상: 실행 원인
- 먼저 볼 근거
- DevTools Performance의 작업·렌더링 기록
- 이것만으로 알 수 없는 것
- 전체 사용자 집단의 분포
- 대상: 실사용 환경
- 먼저 볼 근거
- Speed Insights 등의 실제 사용자 지표
- 이것만으로 알 수 없는 것
- 느린 함수의 구체적인 실행 원인
- 대상: 번들 크기
- 먼저 볼 근거
- 사용한 번들러의 모듈별 분석
- 이것만으로 알 수 없는 것
- 모듈 크기만으로 실제 실행 시간
같은 페이지·기기·네트워크 조건에서 변경 전후를 비교합니다. 이 표는 도구의 범위를 설명하며 북스토어의 측정 결과를 담지 않습니다.
측정으로 주장할 범위 정하기 다음 값은 연습을 위한 가상 관측입니다. 동일 기기·네트워크에서 클라이언트 번들 크기는 200 KB에서 160 KB로 줄었지만, 같은 검색 동작의 상호작용 지연은 210 ms에서 260 ms로 늘었습니다. 번들 감소율과 지연 변화는 얼마이며, 이 결과만으로 검색 응답이 개선됐다고 할 수 있을까요?
각 근거가 답하는 질문
번들은 (200 - 160) / 200 × 100 = 20% 감소했지만, 이 관측의 지연은 50 ms 늘었습니다. 다운로드 크기 감소와 상호작용 개선은 같은 결론이 아닙니다. 원인을 찾으려면 같은 검색 동작의 Performance 기록과 서버·DB 요청 시간을 연결해 봅니다. 단일 관측으로 전체 사용자 분포나 장기 효과까지 확정하지 않습니다.
변경 전후에는 라우트, 데이터 수, 로그인 상태, 캐시를 처음 사용하는 상태와 재사용하는 상태, 기기와 네트워크를 함께 기록합니다. 원인의 위치를 좁힌 뒤 같은 조건의 반복 측정으로 개선 여부를 판단합니다.
Lighthouse 기준선 진단
- 용도: 성능, 접근성, 권장 사항, SEO를 진단합니다. PWA 카테고리는 Lighthouse 12에서 제거되었으므로 설치·서비스 워커 동작은 별도로 확인합니다.
- 사용법: Chrome 개발자 도구에서 Lighthouse 탭을 클릭하고 Analyze page load를 실행합니다.
- 활용: Lighthouse 로딩 검사에서 LCP·CLS·TBT 등을 확인합니다. 사용자 입력이 없는 한 번의 로딩 검사로 INP를 측정하지는 못하므로 상호작용 기록이나 실제 사용자 지표를 함께 사용합니다.
Chrome DevTools (성능 탭, 네트워크 탭)
- Performance 탭: 런타임 성능을 기록하고 분석하여 JavaScript 실행, 렌더링, 레이아웃 계산 등에서 발생하는 병목 현상을 시각적으로 파악합니다.
- Network 탭: 페이지 로딩 시 발생하는 모든 네트워크 요청(JS, CSS, 이미지, API 호출)을 분석하고, 각 리소스의 크기, 로딩 시간, 캐싱 여부 등을 확인합니다. 이를 통해 불필요한 요청이나 큰 리소스를 식별할 수 있습니다.
Vercel Analytics / Speed Insights
Vercel은 배포된 Next.js 애플리케이션에 대한 내장 분석 및 성능 지표를 제공합니다.
- Vercel Analytics: 방문자 수, 페이지 뷰, 트래픽 소스 등 일반적인 웹사이트 분석 데이터를 제공합니다.
- Speed Insights: 실제 사용자 데이터(RUM - Real User Monitoring)를 기반으로 Core Web Vitals를 포함한 페이지 성능 지표를 모니터링합니다. Vercel 대시보드에서 기능을 활성화하고
@vercel/speed-insights를 설치한 뒤 Root Layout에<SpeedInsights />를 렌더링해야 데이터를 수집합니다.
번들 분석기 (Bundle Analyzer)
@next/bundle-analyzer는 Webpack 빌드용 도구입니다. 아래 예제는 Next.js 16에서도 --webpack으로 빌드할 때 적용합니다. Turbopack을 분석할 때는 설치된 Next.js가 제공하는 별도 분석 기능을 사용하며 이 플러그인의 보고서와 섞지 않습니다.
어떤 모듈이 가장 많은 공간을 차지하는지 파악하여 불필요한 코드나 큰 라이브러리를 식별하고 제거할 수 있습니다.
- 설치:
npm install --save-dev @next/bundle-analyzer next.config.js설정: CommonJS 형식의 예시입니다. 기존next.config.ts등과 두 설정 파일을 함께 두지 말고, 선택한 형식의 한 파일에 기존 설정을 보존해 결합합니다.next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({ // Next.js config options });- macOS·Linux에서 실행:
ANALYZE=true npm run build -- --webpack - PowerShell에서 실행:
$env:ANALYZE='true'; npm run build -- --webpack
빌드가 끝나면 번들 분석 보고서가 웹 브라우저에 열립니다.
변경 범위와 해결하려는 병목을 연결하고, 수정 뒤에는 해당 기능과 같은 조건의 측정값을 함께 확인합니다.
추가 연습: 최적화의 조건과 근거 확인하기
- 앞 관측처럼 번들은 200 KB→160 KB, 검색 지연은 210 ms→260 ms입니다. 감소율과 지연 증가율을 각 원래 값 기준으로 계산하세요. 작은 번들만 근거로 검색이 빨라졌다는 리뷰를 승인할 수 있을까요?
- viewport 폭은 600 px, 실제 책 카드의 이미지 표시 폭은 300 px입니다.
sizes="100vw"가 브라우저에 제시하는 폭은 얼마인가요? 표시 폭이 300 px이라는 CSS를 대신 설정할까요? 어떤 측정·수정이 필요할까요? - 원 MyDashboard와 새 OnDemandDashboard의 open=false 상태를 비교하세요. 둘 다 ssr=false인 차트를 사용한다는 이유로 클릭 전 차트 렌더 조건이 같을까요? 버튼 클릭 후 open=true가 되면 무엇이 달라지고 무엇은 아직 측정해야 할까요?
- 위 ISR 조건에서 0초에 저장한 화면 A, 20초에 바뀐 DB 값 B가 있습니다. 첫 만료 후 요청은 75초이며 재생성은 76초에 성공합니다. 60초에 요청이 없을 때, 75초의 첫 응답, 77초의 응답을 판단하세요. 재생성이 실패했다면 마지막 답은 어떻게 달라질까요?
적용 조건과 측정 범위를 연결한 풀이
- 번들은 40/200×100=20% 감소했고 지연은 50/210×100≈23.81% 증가했습니다. 제시된 지연 관측은 악화됐으므로 검색이 빨라졌다는 결론을 지지하지 않습니다. 같은 조건의 반복 검색 기록으로 작업·렌더링·서버 요청 중 어느 비용이 늘었는지 확인합니다.
- 100vw는 600 px을 힌트로 제시합니다. CSS로 그 폭을 직접 설정하지 않으므로 표시 폭은 여전히 주어진 300 px입니다. 실제 레이아웃에 맞는 sizes를 작성하고 브라우저가 선택한 srcset 후보·다운로드 크기를 비교합니다. 힌트가 두 배라고 실제 파일 크기도 정확히 두 배라는 뜻은 아닙니다. DPR과 후보 목록도 영향을 줍니다.
- 다릅니다. 원 예제는 DynamicChart를 항상 포함하지만 새 예제는 open=false일 때 포함하지 않습니다. ssr=false는 서버 사전 렌더링을 제외하는 조건입니다. open=true가 되면 새 예제도 차트를 포함합니다. 실제 청크 요청 시점·크기·첫 표시 시간·상호작용 비용은 별도 관측해야 합니다.
- 60초에는 요청이 없어 재검증을 시작하지 않습니다. 주어진 stale-while-regenerate 조건에서 75초 응답은 A이고 백그라운드 재생성을 시작합니다. 성공 뒤 77초에는 B입니다. 실패했다면 성공한 기존 A를 유지하며 이후 요청을 계기로 재시도합니다. 이 화면의 캐시 정책과 주문 시 최신 재고 확인은 서로 다른 책임입니다.