Next.js 15 프론트엔드 개발 워크플로우. App Router 페이지, React 19 커스텀 훅, Tailwind CSS 4 컴포넌트, API 연동, 인증 흐름 구현. Web 앱(사용자용)과 Admin 앱(관리자용) 모두 포함. '페이지 추가', '컴포넌트 생성', '훅 작성', 'UI 수정', '프론트엔드', '대시보드', '폼 구현', '반응형 레이아웃' 등을 언급하면 이 스킬을 참조할 것. 백엔드 API 로직이나 Stripe 대시보드 설정과는 구분됨.
stripe-newsletter 프로젝트의 Next.js 프론트엔드 개발 표준 워크플로우.
apps/{app}/src/app/{route}/page.tsx:
// 서버 컴포넌트 (기본) — 데이터 표시만
export default function SomePage() {
return <div>...</div>
}
// 클라이언트 컴포넌트 — 상호작용 필요 시
'use client'
export default function InteractivePage() {
const [state, setState] = useState(...)
return <div>...</div>
}
클라이언트 컴포넌트 사용 기준: useState, useEffect, 이벤트 핸들러, useAuth/useSubscription 등 훅 사용 시.
API 연동 훅의 표준 구조:
interface UseXxxReturn {
readonly data: T | null
readonly isLoading: boolean
readonly error: string | null
readonly mutate: (input: Input) => Promise<void>
readonly refresh: () => Promise<void>
}
export function useXxx(): UseXxxReturn {
const [data, setData] = useState<T | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetch = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const response = await api.get<T>('/endpoint')
if (response.success && response.data) {
setData(response.data)
}
} catch {
setError('Failed to fetch')
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => { fetch() }, [fetch])
return { data, isLoading, error, mutate, refresh: fetch }
}
import { api } from '@/lib/api'
// GET
const response = await api.get<User[]>('/users')
// POST
const response = await api.post<{ id: string }>('/users', { name, email })
// PATCH
const response = await api.patch<User>(`/users/${id}`, { name })
// DELETE
const response = await api.delete(`/users/${id}`)
응답 형식: ApiResponse<T> = { success, data?, error?, meta? }
useAuth() 훅으로 인증 상태 접근AuthProvider가 앱 전체를 감싼다 (providers.tsx)localStorage에 저장 (lib/auth.ts의 getToken/setToken/removeToken)useAuth().isAuthenticated 체크 후 리다이렉트| 항목 | Web (apps/web/) | Admin (apps/admin/) |
|---|---|---|
| 대상 | 일반 사용자 | 관리자 |
| 포트 | 3000 | 3001 |
| 인증 | 일반 사용자 JWT | 관리자 역할 JWT |
| 레이아웃 | Header + 콘텐츠 | Sidebar + 콘텐츠 |
| 핵심 기능 | 구독, 콘텐츠 열람 | CRUD 관리, 통계 |
readonly 적용: { readonly title: string }src/components/ 아래 PascalCaseVitest + Testing Library:
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
describe('ComponentName', () => {
it('renders correctly', () => {
render(<ComponentName prop="value" />)
expect(screen.getByText('expected')).toBeInTheDocument()
})
})
테스트 위치: apps/{app}/__tests__/