프로젝트 아키텍처를 리팩토링하다 보니 어떤 곳에서는 route.ts를, 어떤 곳에서는 Server Action을 사용하고 있다는 점이 눈에 들어왔다.
리팩토링을 하면서 Server Component, Server Action, route.ts를 ‘언제, 왜 쓰는지’ 기준을 다시 정리해보기로 했다.
Next.js App Router의 서버 로직 3가지
App Router 기준으로 서버 로직을 작성하는 방법은 크게 세 가지다.
- Server Component
-
Server Action (
"use server") -
Route Handler (
app/api/**/route.ts)
Route Handler (route.ts)
app/api/**/route.ts는 URL을 가진 HTTP 엔드포인트를 만든다.
GET /api/auth/confirm
POST /api/webhook/payment
이 URL은 브라우저, 외부 서비스, 혹은 다른 클라이언트에서 직접 호출될 수 있다.
그렇다면 언제 route.ts를 써야 할까?
1️. 외부 시스템이 우리 서버를 호출해야 할 때
-
OAuth / SSO callback
-
결제, 알림 Webhook
이 경우에는 URL 자체가 외부와의 계약이기 때문에 route.ts가 필수다.
2️. Next.js 앱 외부의 클라이언트가 호출해야 할 때
-
모바일 앱
-
별도의 어드민 프론트엔드
-
다른 레포의 웹 애플리케이션
즉, HTTP API로서 명확한 인터페이스가 필요할 때다.
3️. URL 단위의 제어가 필요할 때
-
HTTP status code 제어 (401 / 403 / 429)
-
header 제어
-
redirect 처리
-
명시적인 캐싱 규칙
이런 경우는 Server Action이나 Server Component로는 표현하기 어렵다.
Server Component
Server Component는 페이지를 렌더링하기 위해 필요한 데이터를 서버에서 조회하고, 그 결과를 바로 JSX로 그리는 역할을 한다.
Server Component가 담당하는 것들
-
목록 조회
-
상세 조회
-
초기 화면 데이터
-
화면 단위 데이터 조합 (BFF 역할)
이런 로직은 Server Action이 아니라 Server Component의 책임이다.
Server Action
Server Action은 UI 이벤트를 서버에서 처리하기 위한 장치다.
Server Action이 잘 어울리는 경우
-
버튼 클릭
-
form submit
-
생성 / 수정 / 삭제
-
쿠키 / 세션 기반 인증
-
DB 직접 접근이 필요한 변경 작업
Server Action은 API라기 보다는 UI에서 발생한 행동을 서버로 전달하는 통로에 가깝다.
외부에서 이 로직을 URL로 직접 호출할 이유가 없다면 굳이 route.ts를 만들 필요는 없다.
내가 정리해본 기준
여러 번 헷갈리다 보니,
기준을 한 번에 정리하려 하기보다는 단계를 나눠서 생각하는 편이 훨씬 편했다.
그래서 이렇게 두 가지 질문으로 정리해봤다.
1️. 이 로직은 URL로 직접 호출될 필요가 있을까?
- 있다 →
route.ts - 없다 → Server Component 또는 Server Action
2. URL로 노출되지 않는다면, 이 로직은 무엇을 하고 있을까?
- 페이지를 그리기 위한 조회라면 → Server Component
- UI 이벤트로 상태를 변경한다면 → Server Action
우리 프로젝트에 적용해보면
우리 프로젝트는 별도의 백엔드 서버 없이 Next.js + Supabase로 구성되어 있다.
이 구조에서는 대부분의 서버 로직을 굳이 URL로 노출할 필요가 없다.
예를 들어, 예전에 만들었던 아래 같은 API들은
POST /api/diary/createPOST /api/diary/updatePOST /api/diary/delete
다시 생각해보면 공통점이 꽤 분명하다.
- 사용자 본인 데이터만 다룬다
- 내부 UI 흐름에서만 호출된다
- 버튼 클릭이나 폼 제출로 발생한다
- DB에 직접 접근하는 변경 작업이다
이런 특징을 놓고 보면 이 로직들은 API라기보다는 UI 이벤트 처리에 가깝다.
그래서 route.ts로 분리하기보다는 Server Action으로 처리하는 쪽이 훨씬 자연스럽다.
features/diary/actions/createDiary.ts // Server Action (WRITE)
features/diary/actions/updateDiary.ts // Server Action (WRITE)
features/diary/queries/getDiaryList.ts // Server Component 전용 (READ)
features/tag/queries/getTags.ts // Server Component 전용 (READ)
또한 이 조회 로직들이 특정 페이지에서만 사용된다면,
굳이 분리하지 않고 Server Component 안에서 바로 조회해도 충분하다.
정리
Next.js App Router를 사용하면서
프론트엔드와 서버를 명확히 나누던 예전 방식이 더 이상 잘 맞지 않는다는 걸 느꼈다.
API를 먼저 설계하기보다, 이 로직이 어디에 속하는지, 어떤 역할을 맡아야 하는지부터 정하는 것 이 App Router를 제대로 활용하는 방법이라고 느꼈다.