3.1 Authenticate Server Actions Like API Routes
- 핵심: Server Action 내부에서 인증/인가를 반드시 검증한다.
- 왜 중요한가: 액션은 엔드포인트처럼 호출 가능하므로 보안 경계가 필요하다.
- 코드리뷰 신호: action 함수 내부에 auth/authorization 검증이 없다.
action 함수 내부에 auth/authorization 검증이 없다 코드는 보통 구현 편의 때문에 생기지만, 트래픽이 늘면 병목으로 확대된다. 이 항목의 핵심은 문법 변경이 아니라 실행/렌더 흐름을 정리해 Server Action 내부에서 인증/인가를 반드시 검증한다를 기본값으로 만드는 데 있다.
변경 뒤에는 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률와 회귀 테스트를 같이 보면서 액션은 엔드포인트처럼 호출 가능하므로 보안 경계가 필요하다 결과가 재현되는지 점검한다.
예시 (Bad) ▼
'use server'
export async function deleteUser(userId: string) {
// 누구나 호출 가능! 인증 체크 없음
await db.user.delete({ where: { id: userId } })
return { success: true }
}
예시 (Good) ▼
'use server'
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
export async function deleteUser(userId: string) {
// 액션 내부에서 항상 인증 체크
const session = await verifySession()
if (!session) {
throw unauthorized('Must be logged in')
}
// 인가도 함께 체크
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
}
await db.user.delete({ where: { id: userId } })
return { success: true }
}
3.2 Avoid Duplicate Serialization in RSC Props
- 핵심: RSC 경계에서 동일 데이터 직렬화 중복을 줄인다.
- 왜 중요한가: payload 크기를 줄여 전송과 hydration 부담을 낮춘다.
- 코드리뷰 신호: 동일 데이터 파생 객체를 여러 개 client props로 전달한다.
코드리뷰에서 동일 데이터 파생 객체를 여러 개 client props로 전달한다. 신호가 보이면 먼저 책임 경계를 다시 나누는 것이 우선이다.
책임이 정리되면 요청 경계에서 인증, 직렬화, 캐시 책임을 분리해 중복 I/O를 줄인다 이 원칙을 자연스럽게 적용할 수 있고, RSC 경계에서 동일 데이터 직렬화 중복을 줄인다 규칙도 팀 기준으로 고정하기 쉬워진다.
마지막으로 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 기준으로 payload 크기를 줄여 전송과 hydration 부담을 낮춘다 개선이 체감과 일치하는지 확인한다.
예시 (Bad) ▼
// RSC: 문자열 6개 전송 (배열 2개 × 항목 3개)
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
예시 (Good) ▼
// RSC: 한 번만 전송
<ClientList usernames={usernames} />
// 클라이언트에서 변환 처리
'use client'
const sorted = useMemo(() => [...usernames].sort(), [usernames])
3.3 Cross-Request LRU Caching
- 핵심: 요청 간 재사용 데이터는 LRU 캐시를 둔다.
- 왜 중요한가: 반복 DB/API 호출을 줄여 서버 지연과 비용을 절감한다.
- 코드리뷰 신호: 동일 키 조회가 요청마다 반복된다.
동일 키 조회가 요청마다 반복된다 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 요청 경계에서 인증, 직렬화, 캐시 책임을 분리해 중복 I/O를 줄인다. 이렇게 구조를 잡아두면 요청 간 재사용 데이터는 LRU 캐시를 둔다 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 확인해 반복 DB/API 호출을 줄여 서버 지연과 비용을 절감한다 효과가 실제로 나타나는지 검증한다.
예시 (Bad) ▼
async function getUserProfile(userId: string) {
// 같은 사용자가 반복 조회되어도 매번 DB 호출
return db.user.findUnique({
where: { id: userId },
include: { team: true, permissions: true }
})
}
예시 (Good) ▼
type CacheEntry<T> = { value: T; expiresAt: number }
const userCache = new Map<string, CacheEntry<UserProfile>>()
const USER_CACHE_TTL = 60_000
const USER_CACHE_MAX = 500
async function getUserProfile(userId: string) {
const cached = userCache.get(userId)
const now = Date.now()
if (cached && cached.expiresAt > now) {
return cached.value
}
const profile = await db.user.findUnique({
where: { id: userId },
include: { team: true, permissions: true }
})
userCache.set(userId, { value: profile, expiresAt: now + USER_CACHE_TTL })
if (userCache.size > USER_CACHE_MAX) {
const oldestKey = userCache.keys().next().value
if (oldestKey) userCache.delete(oldestKey)
}
return profile
}
3.4 Hoist Static I/O to Module Level
- 핵심: 정적 I/O는 요청 핸들러 밖(모듈 스코프)으로 hoist한다.
- 왜 중요한가: 매 요청 반복 I/O를 제거할 수 있다.
- 코드리뷰 신호: 매 요청마다 같은 파일/템플릿을 다시 읽는다.
실무에서 매 요청마다 같은 파일/템플릿을 다시 읽는다. 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다. 작은 단위(한 페이지, 한 훅, 한 API)부터 정적 I/O는 요청 핸들러 밖(모듈 스코프)으로 hoist한다 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다.
이후 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 배포 전후로 비교해 매 요청 반복 I/O를 제거할 수 있다 개선이 지속되는지 확인한다.
예시 (Bad) ▼
import { readFile } from 'node:fs/promises'
export async function GET() {
// 모든 요청에서 동일 파일을 다시 읽음
const emailTemplate = await readFile('./templates/welcome.html', 'utf-8')
return Response.json({ emailTemplate })
}
예시 (Good) ▼
import { readFile } from 'node:fs/promises'
// 서버 시작 후 한 번만 로드
const emailTemplatePromise = readFile('./templates/welcome.html', 'utf-8')
export async function GET() {
const emailTemplate = await emailTemplatePromise
return Response.json({ emailTemplate })
}
3.5 Minimize Serialization at RSC Boundaries
- 핵심: Client에 필요한 최소 필드만 전달한다.
- 왜 중요한가: RSC/HTML payload 축소는 초기 렌더 개선에 직접적이다.
- 코드리뷰 신호: 대형 엔티티 전체를 client props로 넘긴다.
대형 엔티티 전체를 client props로 넘긴다 코드는 보통 구현 편의 때문에 생기지만, 트래픽이 늘면 병목으로 확대된다. 이 항목의 핵심은 문법 변경이 아니라 실행/렌더 흐름을 정리해 Client에 필요한 최소 필드만 전달한다를 기본값으로 만드는 데 있다.
변경 뒤에는 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률와 회귀 테스트를 같이 보면서 RSC/HTML payload 축소는 초기 렌더 개선에 직접적이다 결과가 재현되는지 점검한다.
예시 (Bad) ▼
async function Page() {
const user = await fetchUser() // 필드 50개
return <Profile user={user} />
}
'use client'
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // 실제 사용 필드 1개
}
예시 (Good)
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
}
'use client'
function Profile({ name }: { name: string }) {
return <div>{name}</div>
}
3.6 Parallel Data Fetching with Component Composition
- 핵심: 컴포넌트 구조를 바꿔 서버 fetch 시작 시점을 병렬화한다.
- 왜 중요한가: 트리 구조 자체가 대기 순서를 결정하므로 효과가 크다.
- 코드리뷰 신호: 상위 fetch 완료 후에야 하위 fetch가 시작된다.
코드리뷰에서 상위 fetch 완료 후에야 하위 fetch가 시작된다. 신호가 보이면 먼저 책임 경계를 다시 나누는 것이 우선이다. 책임이 정리되면 요청 경계에서 인증, 직렬화, 캐시 책임을 분리해 중복 I/O를 줄인다. 이 원칙을 자연스럽게 적용할 수 있고, 컴포넌트 구조를 바꿔 서버 fetch 시작 시점을 병렬화한다. 규칙도 팀 기준으로 고정하기 쉬워진다.
마지막으로 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 기준으로 트리 구조 자체가 대기 순서를 결정하므로 효과가 크다 개선이 체감과 일치하는지 확인한다.
예시 (Bad) ▼
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
예시 (Good) ▼
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
3.7 Per-Request Deduplication with React.cache()
- 핵심: 동일 요청 내 중복 async 작업은 React.cache()로 dedupe한다.
- 왜 중요한가: 같은 요청의 중복 DB/Auth/연산 비용을 제거한다.
- 코드리뷰 신호: 동일 함수를 여러 컴포넌트가 같은 요청에서 반복 호출한다.
동일 함수를 여러 컴포넌트가 같은 요청에서 반복 호출한다. 징후는 기능을 빠르게 추가할 때 쉽게 누적된다. 먼저 요청 경계에서 인증, 직렬화, 캐시 책임을 분리해 중복 I/O를 줄인다. 이렇게 구조를 잡아두면 동일 요청 내 중복 async 작업은 React.cache()로 dedupe한다. 원칙을 예외 없이 유지하기가 훨씬 쉬워진다.
적용 후에는 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 확인해 같은 요청의 중복 DB/Auth/연산 비용을 제거한다 효과가 실제로 나타나는지 검증한다.
예시 (Bad) ▼
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
// 호출마다 새 객체 생성으로 캐시 적중 불가
getUser({ uid: 1 })
getUser({ uid: 1 }) // 캐시 미스, 쿼리 재실행
예시 (Good)
const params = { uid: 1 }
getUser(params) // 쿼리 실행
getUser(params) // 캐시 히트 (같은 참조)
3.8 Use after() for Non-Blocking Operations
- 핵심: 로그/후처리 등 비핵심 작업은 after()로 응답 이후 처리한다.
- 왜 중요한가: 핵심 응답 latency를 줄일 수 있다.
- 코드리뷰 신호: 부수효과를 응답 전에 await한다.
실무에서 부수효과를 응답 전에 await한다 부분만 단발성으로 고치면 같은 문제가 다시 나오기 쉽다. 작은 단위(한 페이지, 한 훅, 한 API)부터 로그/후처리 등 비핵심 작업은 after()로 응답 이후 처리한다. 패턴을 적용하고, 경계 케이스를 함께 정리해야 회귀를 줄일 수 있다.
이후 서버 응답 시간, DB/외부 API 호출 수, 캐시 적중률을 배포 전후로 비교해 핵심 응답 latency를 줄일 수 있다. 개선이 지속되는지 확인한다.
예시 (Bad) ▼
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// 변경 작업 수행
await updateDatabase(request)
// 로깅이 응답을 블로킹
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
예시 (Good) ▼
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// 변경 작업 수행
await updateDatabase(request)
// 응답 전송 후 로깅
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}
'Develop > Web' 카테고리의 다른 글
| [React Best Practices] 2. 번들 크기 최적화 (0) | 2026.04.17 |
|---|---|
| [React Best Practices] 1. 워터폴 제거 (0) | 2026.03.29 |
| [Web][React] 디자인 시스템 JSDoc에 스크린샷 자동 주입하기 (1) | 2026.01.13 |
| [Web] JSDoc과 Storybook으로 컴포넌트 문서화 (0) | 2025.12.25 |
| [Web] 시맨틱 태그 적용 작업 (2) | 2025.12.12 |