Next.js로 프로젝트를 시작할 때 가장 흔한 문제는 “일단 돌아가게” 만든 뒤 시간이 지나면서 구조가 무너지는 것입니다. 처음에는 pages/app, 컴포넌트, API 라우트, 상태 관리 코드가 적당히 섞여 있어도 개발 속도가 나옵니다. 하지만 팀원이 늘거나 기능이 쌓이면 폴더 기준이 흔들리고, 타입 규칙이 제각각이 되고, 배포 환경에서만 터지는 버그가 늘어납니다.
Next.js와 TypeScript 조합을 “예쁘게” 구성하는 방법이 아니라, 운영과 협업에서 흔들리지 않는 기준을 세울수 있도록 도움이 되면좋겠습니다.
아래 내용으로 설명합니다.
- 프로젝트 구조를 어떻게 나눠야 변경 비용이 줄어드는지
- TypeScript 설정을 어떤 방향으로 가져가야 운영 버그를 줄이는지
- 환경변수, API 경계, 공용 타입을 어떻게 관리해야 안전한지
- 팀에서 지키기 쉬운 규칙(ESLint/Prettier/경로 alias)을 어떻게 고정하는지
개념/배경 설명
Next.js는 프레임워크가 제공하는 기능이 많습니다. 라우팅, SSR/SSG, API 라우트, 서버 컴포넌트/클라이언트 컴포넌트 분리, 번들링 최적화까지 포함되다 보니 “프로젝트 구성”이 곧 “설계”가 됩니다.
TypeScript는 그 설계를 코드로 강제하는 도구입니다. 문제는 TypeScript를 “타입 붙이는 도구”로만 쓰면, 오히려 타입이 중복되고 경계가 흐려지면서 유지보수성이 떨어질 수 있습니다. 실무에서는 다음이 중요합니다.
- 타입은 정확성을 높이되, 변경 비용을 폭증시키지 않도록 경계를 세운다
- 런타임 입력(환경변수/요청/응답)은 타입이 아니라 검증이 필요하다는 전제를 둔다
- 코드가 늘어나도 폴더 기준이 흔들리지 않게 “영역 분리”를 먼저 한다
잘못 구성하면 생기는 문제는 매우 현실적입니다.
- 컴포넌트가 비즈니스 로직까지 품고 테스트/재사용이 어려워진다
- API 응답 타입이 화면과 결합되어 서버 변경이 프론트 전체 변경으로 번진다
- 환경변수 오타가 배포 이후에야 발견된다
- 서버/클라이언트 코드가 섞여 번들에 비밀값이 포함되는 사고가 난다
핵심 설계/전략 섹션
1) 폴더 구조는 “기술 계층”보다 “영역 경계”를 먼저 잡는다
초기에는 components, hooks, utils 같은 기술 계층 중심으로 나누기 쉽습니다. 하지만 기능이 늘면 “이 기능은 어디에 두어야 하는가”가 애매해지고, 결국 아무 데나 들어가기 시작합니다. 실무에서는 기능(도메인) 기준의 경계와 공통 모듈 경계를 먼저 정하는 편이 안전합니다.
권장 구조 예시는 다음과 같습니다. 팀/환경에 따라 달라질 수 있지만, 경계 개념은 유지하는 것이 핵심입니다.
src/
app/ // Next.js 라우팅(페이지/레이아웃)
features/ // 도메인 기능 단위(예: auth, billing, profile)
auth/
components/
services/
hooks/
types.ts
shared/ // 기능과 무관한 공통 모듈
ui/
lib/
types/
server/ // 서버 전용 코드(비밀키/DB/외부 API)
db/
auth/
telemetry/
config/ // 앱 설정(환경변수 파싱, 상수)
tests/
실무 포인트 정리
- 기능이 커질수록 “features” 같은 도메인 경계가 변경 비용을 줄여줍니다.
- 서버 전용 코드는 “server”로 분리해 클라이언트 번들 포함 사고를 줄입니다.
- 공용(shared)은 무조건 늘어나기 때문에, 초기부터 기준을 강하게 잡아야 합니다.
2) App Router 환경에서 서버/클라이언트 경계를 파일 레벨로 고정한다
Next.js에서는 서버에서 실행되는 코드와 브라우저에서 실행되는 코드가 섞이기 쉽습니다. 실무에서 문제가 되는 지점은 “서버에서만 안전한 값”이 실수로 클라이언트 번들에 포함되는 경우입니다.
권장하는 기준은 단순합니다.
- 서버 전용 로직(토큰 서명, 외부 API 비밀키, DB 접근)은 src/server 아래에서만 작성
- 서버 전용 모듈을 클라이언트 컴포넌트에서 import 하지 못하도록 린트 규칙으로 막기
- 클라이언트 컴포넌트는 UI와 사용자 상호작용만 담당하고, 비즈니스 로직은 services로 이동
실무 포인트 정리
- 경계를 문서로만 두지 말고, 폴더/규칙/린트로 강제해야 운영 사고가 줄어듭니다.
- 서버 로직이 프론트에 섞이면 디버깅 비용이 급격히 증가합니다.
3) TypeScript 설정은 “엄격함”과 “운영 효율”의 균형을 잡는다
TypeScript의 strict를 켜는 것이 항상 정답처럼 보이지만, 팀의 숙련도와 코드베이스 상태에 따라 도입 전략은 달라질 수 있습니다. 다만 운영 안정성을 생각하면, 애매한 any와 느슨한 null 처리로 인해 런타임에서 터지는 경우가 가장 손해입니다.
권장 tsconfig 방향은 다음과 같습니다.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
여기서 중요한 이유는 두 가지입니다. 첫째, index 접근과 optional 처리에서 실수를 줄여 실제 장애를 막습니다. 둘째, 경로 alias를 고정하면 리팩터링과 모듈 이동이 쉬워져 유지보수 비용이 낮아집니다.
실무 포인트 정리
- 엄격 옵션은 “처음부터 전부”가 부담이면, 신규 코드부터 적용하는 단계적 도입도 가능합니다(팀/환경에 따라 달라질 수 있음).
- 경로 alias는 작은 팀에서도 효과가 큽니다. 나중에 바꾸면 비용이 커집니다.
4) 환경변수는 “타입 선언”이 아니라 “런타임 검증”이 핵심이다
환경변수는 빌드/배포 환경에서 주입되므로, TypeScript 타입만으로 안전해지지 않습니다. 실무에서 자주 터지는 장애는 키가 비어 있거나 오타가 있는 경우입니다. 그래서 환경변수는 앱 시작 시점에 검증하고, 실패하면 빠르게 죽게 만드는 편이 안전합니다.
type Env = {
NODE_ENV: 'development' | 'test' | 'production';
API_BASE_URL: string;
INTERNAL_API_KEY: string;
};
function requireEnv(key: string): string {
const v = process.env[key];
if (!v) throw new Error(`Missing env: ${key}`);
return v;
}
export const env: Env = {
NODE_ENV: (process.env.NODE_ENV as Env['NODE_ENV']) ?? 'development',
API_BASE_URL: requireEnv('API_BASE_URL'),
INTERNAL_API_KEY: requireEnv('INTERNAL_API_KEY'),
};
이 코드는 단순하지만 운영에서 효과가 큽니다. 배포 후에야 발견되는 “값이 비어서 기능이 안 됨” 같은 장애를 배포 직후, 또는 서버 부팅 시점에 바로 잡아낼 수 있습니다.
실무 포인트 정리
- 환경변수는 반드시 런타임에서 검증해야 합니다.
- 서버 전용 환경변수는 server 폴더에서만 참조하도록 경계를 지키는 것이 안전합니다.
5) API 경계 타입은 “공유”하되 “결합”은 피한다
Next.js 프로젝트에서 프론트와 백이 같은 레포에 있으면 타입을 공유하고 싶어집니다. 공유 자체는 생산성을 올리지만, 잘못하면 UI 타입이 서버 계약을 오염시키고 서버 변경이 UI 전체 변경으로 번집니다.
권장 방식은 API 계약 타입을 “전용 파일”로 두고, UI 타입과 분리하는 것입니다. 예시는 다음과 같습니다.
// src/shared/types/api.ts
export type ApiResponse<T> =
| { ok: true; data: T }
| { ok: false; error: { code: string; message: string } };
export type SessionListItem = {
sessionId: string;
deviceLabel: string;
lastUsedAt: string;
status: 'ACTIVE' | 'REVOKED';
};
여기서 핵심은 “API 응답은 항상 실패 가능”이라는 점을 타입으로 강제하는 것입니다. 운영에서 장애가 날 때 프론트가 예외 처리를 안 해서 2차 장애(화이트 스크린)가 나오는 경우가 흔합니다.
실무 포인트 정리
- API 타입은 공용(shared)에 두되, UI 전용 타입과 섞지 않습니다.
- 성공/실패를 유니온 타입으로 강제하면 예외 처리가 표준화됩니다.
코드 예제
아래 코드는 “멀티 디바이스 세션 목록 조회 + 모든 기기 로그아웃” 같은 실제 기능을 만든다는 가정으로, Next.js 프로젝트에서 TypeScript 기반으로 흔히 사용하는 패턴을 보여줍니다. 프레임워크 세부(라우트 파일 위치 등)는 팀 구성에 따라 달라질 수 있지만, 핵심은 경계와 타입입니다.
1) 서버 전용 서비스 모듈
// src/server/auth/sessionService.ts
import { env } from '@/config/env';
import type { ApiResponse, SessionListItem } from '@/shared/types/api';
export async function fetchSessions(userId: string): Promise<ApiResponse<SessionListItem[]>> {
const res = await fetch(`${env.API_BASE_URL}/internal/sessions?userId=${encodeURIComponent(userId)}`, {
headers: { 'x-internal-key': env.INTERNAL_API_KEY },
cache: 'no-store',
});
if (!res.ok) {
return { ok: false, error: { code: 'UPSTREAM_ERROR', message: `status=${res.status}` } };
}
const data = (await res.json()) as SessionListItem[];
return { ok: true, data };
}
export async function revokeAllSessions(userId: string): Promise<ApiResponse<{ revokedCount: number }>> {
const res = await fetch(`${env.API_BASE_URL}/internal/sessions/revoke-all`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-internal-key': env.INTERNAL_API_KEY,
},
body: JSON.stringify({ userId }),
cache: 'no-store',
});
if (!res.ok) {
return { ok: false, error: { code: 'UPSTREAM_ERROR', message: `status=${res.status}` } };
}
const data = (await res.json()) as { revokedCount: number };
return { ok: true, data };
}
이 예시에서 중요한 점은 서버 전용 env를 src/server 아래에서만 사용한다는 것입니다. 또한 API 응답을 성공/실패 유니온으로 고정해, 호출자가 반드시 분기 처리하도록 만드는 것이 운영 사고를 줄입니다.
2) UI 컴포넌트에서는 “결과 처리”에 집중
// src/features/auth/components/SessionPanel.tsx
'use client';
import { useEffect, useState } from 'react';
import type { SessionListItem } from '@/shared/types/api';
type Props = {
loadSessions: () => Promise<{ ok: true; data: SessionListItem[] } | { ok: false; message: string }>;
logoutAll: () => Promise<{ ok: true; revokedCount: number } | { ok: false; message: string }>;
};
export function SessionPanel({ loadSessions, logoutAll }: Props) {
const [items, setItems] = useState<SessionListItem[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
void (async () => {
const r = await loadSessions();
if (!r.ok) {
setError(r.message);
return;
}
setItems(r.data);
})();
}, [loadSessions]);
return (
<div>
<h3>세션 목록</h3>
{error && <p>오류: {error}</p>}
<ul>
{items.map((s) => (
<li key={s.sessionId}>
{s.deviceLabel} / {s.status} / {s.lastUsedAt}
</li>
))}
</ul>
<button
onClick={async () => {
const r = await logoutAll();
if (!r.ok) {
setError(r.message);
return;
}
setItems([]);
}}
>
모든 기기에서 로그아웃
</button>
</div>
);
}
UI 쪽에서 서버 로직을 직접 import 하지 않고, 주입받는 형태로 만들면 경계가 깨지는 것을 줄일 수 있습니다. 또한 데이터 타입을 API 타입으로 통일해두면 화면에서의 타입 중복이 줄고, 변경이 들어와도 영향 범위를 제어하기 쉬워집니다.
운영/실무에서 자주 겪는 문제
프로젝트 구성이 무너지면서 자주 발생하는 문제는 대체로 비슷합니다.
- 서버 전용 모듈을 클라이언트에서 import 해서 비밀키가 노출되는 사고
- features 경계 없이 공용(shared)만 비대해져서 어디서든 접근 가능한 구조
- 환경변수 누락이 런타임에서 조용히 실패해 원인 파악이 늦어지는 문제
- API 응답 실패 케이스가 누락되어 특정 상황에서만 화면이 깨지는 문제
결국 프로젝트 구성은 “예쁘게 정리”가 아니라 “실수를 막는 장치”여야 합니다. 코드가 늘어날수록 사람은 실수하고, 실수는 운영에서 비용으로 돌아옵니다.
실무 권장 체크리스트
- src 기준으로 폴더 경계(features/shared/server)가 명확한가
- 서버 전용 코드와 클라이언트 코드가 폴더 레벨에서 분리되어 있는가
- tsconfig strict와 주요 안정성 옵션이 적용되어 있는가
- 환경변수가 런타임 검증으로 보호되고 있는가
- API 타입이 UI 타입과 분리되어 있으며, 성공/실패 처리가 강제되는가
- 경로 alias(@/*)가 고정되어 있고, import 규칙이 통일되어 있는가
Next.js와 TypeScript 프로젝트 구성을 잘한다는 것은 폴더를 멋지게 나누는 것이 아니라, 운영에서 자주 터지는 실수를 “구조적으로 막는 것”이라 생각하면 좋을것 같습니다.
경계를 먼저 정하고, 타입을 계약으로 사용하고, 런타임 입력은 검증하며, 팀이 지키기 쉬운 규칙으로 고정하면 유지보수와 안정성이 함께 올라갑니다. 결국 이렇게 구성하면 기능이 늘어도 구조가 흔들리지 않고, 변경이 들어와도 영향 범위내에서 적용 가능합니다.
'개발 > Typescript' 카테고리의 다른 글
| [TYPESCRIPT] Next.js Server Actions vs API Route 선택 전략: 권한·트랜잭션·에러 처리 관점 비교 (0) | 2026.01.17 |
|---|---|
| [TYPESCRIPT] Next.js 데이터 접근 계층 설계: fetch 래퍼·에러 표준화·캐시 전략 (0) | 2026.01.16 |
| [TYPESCRIPT] 실무 기준 인증 구조 점검 체크리스트: 초기 서비스부터 성장 단계까지 설계 기준 정리 (0) | 2026.01.14 |
| [TYPESCRIPT] 운영 기준 인증 장애 대응 전략: 실제 장애 시나리오로 보는 대응 흐름과 점진적 개선 방법 (0) | 2026.01.13 |
| [TYPESCRIPT] 운영 기준 인증 모니터링 전략: 로그인·토큰·보안 이벤트를 지표로 관리하는 실무 설계 (0) | 2026.01.12 |
