프론트엔드와 백엔드 서버 구조¶
한 줄 요약: 브라우저에서 도는 코드는 누구나 열어 볼 수 있다. 그래서 비밀값을 들고 대신 요청해 주는 서버가 필요하다. Next.js는 그 자체가 Node.js 서버로 돌기 때문에 화면과 그 서버 역할을 함께 맡을 수 있다.
📍 만난 상황¶
- 2026-11-09 — 2번 물음 → 그날 일지
- 14:10 대시보드를 외부 앱처럼 따로 만들기로 했다. 백엔드는 이미 dhub2-manager가 있는데 왜 서버를 또 만드는지 물었다.
- 15:54 백엔드 계획을 짜다가 "그냥 Next.js로 서버 없이 하는 건 불가능한가?"라고 물었다.
- 결론: 매니저 앞에 붙는 얇은 중계 서버가 필요하고, 그 역할을 Next.js 하나가 화면과 함께 맡기로 했다.
- 2026-11-10 — 1번 물음 → 그날 일지
- 13:23 아키텍처 설명을 준비하다가 "Next.js의 서버 코드가 BFF 역할을 한다"는 답을 들었다. 이어서 "Next.js로 프론트와 백을 둘 다 할 수 있는 이유는? 나중에 복잡해져도 유지할 수 있나?"를 물었다.
- 지난번과 달라진 점: "서버가 왜 필요한가"에서 한 단계 나아가, 한 프로그램이 어떻게 둘 다 하는지와 어디까지 버티는지를 물었다.
🧩 쉽게 말하면¶
식당을 떠올리면 된다. 손님 테이블(브라우저)에 금고 열쇠(비밀값)를 둘 수는 없다. 손님은 홀 직원(서버)에게 주문만 한다. 직원이 열쇠를 들고 주방(매니저)에 가서 음식을 받아 온다.
Next.js는 메뉴판을 나눠 주는 일과 주문을 받아 오는 일을 한 사람이 하는 가게다. 직원이 둘일 필요가 없어 작은 가게에 편하다. 다만 "새벽마다 장 보기"나 "케이크 3시간 굽기" 같은 일은 이 직원 혼자 맡기 어렵다.
📖 제대로 알기¶
프론트엔드와 백엔드¶
- 프론트엔드: 이용자 브라우저에서 도는 코드다. 화면을 그리고 클릭을 받는다. 누구나 개발자 도구로 내용을 볼 수 있다.
- 백엔드: 서버 컴퓨터에서 도는 코드다. 이용자는 결과만 받는다.
- 브라우저에 두면 안 되는 것: 토큰(접근 권한 문자열), client secret(로그인 서버에 앱임을 증명하는 비밀값), DB 비밀번호.
CORS¶
CORS(Cross-Origin Resource Sharing: 다른 주소 사이의 요청 허용 규칙)는 브라우저가 지키는 보안 규칙이다. 페이지 주소와 다른 주소로 요청하면, 받는 쪽이 허락해야 결과를 볼 수 있다. dhub2-manager는 등록된 주소만 받으므로 브라우저에서 바로 부를 수 없다.
서버끼리 통신에는 CORS가 걸리지 않는다. 그래서 중계 서버가 대신 부른다. 화면 하나를 위한 이 중계 서버를 BFF라고 부른다. → BFF 패턴
React만 쓸 때 vs Next.js¶
- React만(예: Vite로 만든 앱): 결과물은 HTML·JS 정적 파일뿐이다. 서버가 없으니 비밀을 숨길 곳도, DB에 붙을 곳도 없다. 백엔드를 따로 만들어야 한다.
- Next.js(기본 실행 방식): 빌드 결과에 Node.js 서버 프로그램이 들어 있다. 그 서버가 화면 파일도 보내고 API도 처리한다.
- 예외: 정적 내보내기(
output: 'export')를 쓰면 Next.js도 파일만 만든다. 이때는 서버가 없다.
한 코드베이스가 빌드 때 두 갈래로 나뉜다¶
| 갈래 | 어디서 도나 | 예 |
|---|---|---|
클라이언트 코드 ("use client") |
브라우저 | 조건 패널, 지도, 표, 즐겨찾기 버튼 |
| 서버 코드 | Node.js 서버 | route.ts API, page.tsx의 DB 읽기, src/server/** |
- Route Handler(
app/api/**/route.ts):GET·POST함수를 내보내면 그대로 HTTP API가 된다. Express의app.get(...)과 같은 역할이다. - Server Component(서버에서 미리 그리는 화면 조각,
page.tsx의 기본): 화면을 그리기 전에 서버에서 PG를 직접 읽는다. 별도 API를 거치지 않는다. import "server-only": 이 줄이 있는 파일이 브라우저 쪽에 섞이면 빌드가 실패한다. 토큰·SQL 코드가 새지 않게 막는 안전장치다.- 서버 쪽은 일반 Node.js라
pg드라이버,node:crypto, 환경 변수를 그대로 쓴다.
⚙️ 어떻게 동작하나¶
"침수 · 2022-08 · 강남구"로 건수를 볼 때를 따라가 본다.
- 브라우저가 Next.js의
/api/...로 조건만 보낸다. 같은 주소라 CORS도 없다. - Route Handler가 세션 쿠키로 누구인지 확인한다.
- 서버가 PG에서 데이터셋 정보를 읽고 SQL을 만든다.
- 서버가 자기만 가진 토큰을 붙여 매니저에 보낸다.
- 매니저가 권한을 검사하고 ClickHouse에서 결과를 돌려준다.
- 서버가 결과를 정리해 브라우저에 준다.
flowchart LR
B["브라우저<br/>use client 코드"] -->|"① 조건만"| N["Node 서버<br/>Route Handler"]
N -->|"③ 메타 읽기"| P[("PostgreSQL")]
N -->|"④ SQL + 토큰"| M["dhub2-manager"]
M --> C[("ClickHouse")]
B와 N은 같은 Next.js 프로젝트, 같은 컨테이너에서 나온다. 그래도 B는 브라우저에서, N은 서버에서 돈다. 토큰은 N 쪽에만 있다.
🧪 예시¶
-
예시 1 · 일상에 빗대어: 법인카드(토큰)를 고객 100명에게 나눠 주면 1명만 잃어버려도 끝이다. 직원 1명이 카드를 들고 100명의 주문을 받아 결제하면, 위험한 물건이 있는 곳이 100곳에서 1곳으로 준다.
-
예시 2 · 실제로는: Route Handler 하나의 뼈대다.
// app/api/count/route.ts
import "server-only"; // ①
export async function POST(req: Request) {
const { region, from, to } = await req.json(); // ②
const sql = buildCountSql(region, from, to); // ③
const res = await fetch(`${process.env.MANAGER_URL}/analytics/query`, {
method: "POST",
headers: { Authorization: `Bearer ${await getToken()}` }, // ④
body: JSON.stringify({ sql }),
});
return Response.json(await res.json());
}
route.ts는 원래 서버에서만 돈다. 이 줄은 화면 코드가 이 파일을 실수로 불러오면 빌드를 멈추는 이중 안전장치다. 토큰을 다루는 src/server/** 파일에 특히 붙인다.
② 브라우저가 보낸 것은 조건뿐이다.
③ SQL은 서버가 틀로 만든다. 값은 검사를 거쳐 넣는다.
④ 토큰은 서버 환경 변수·세션에서 꺼낸다. 브라우저 코드에는 없다.
⚖️ 비교 · 흔한 오해¶
| 구조 | 프로그램 수 | 특징 |
|---|---|---|
| React 화면 + FastAPI 백엔드 | 2개 | 역할이 뚜렷이 나뉨, 배포·인증 연결이 2배 |
| Next.js(화면 + 서버 코드) | 1개 | 언어·저장소·컨테이너 하나 |
하는 일은 같고, 몇 개로 나눠 띄우느냐만 다르다.
- 헷갈렸던 점: "Next.js = 프론트엔드 도구" — React 화면을 만드는 데 쓰니 그렇게 보인다. 하지만 기본 실행 방식은 Node 서버다. Express로 만든 백엔드가 할 수 있는 일을 서버 코드에서 그대로 할 수 있다.
- 헷갈렸던 점: "복잡해지면 Next.js를 버려야 한다" — 어떤 복잡함인지에 따라 다르다. 화면이 복잡해지는 건 문제없다. 결국 React이기 때문이다. 문제는 요청-응답 구조에 맞지 않는 백엔드 일이다(아래 표).
- 오해: "CORS를 풀면 해결된다" — 풀어도 토큰을 브라우저에 둬야 하는 문제는 그대로다.
🛠️ 실무에서는¶
Next.js 서버는 "요청이 오면 응답하고 끝" 구조다. 여기에 안 맞는 일은 떼어 낸다.
| 이런 일이 생기면 | Next.js 안에서 | 떼어 내는 곳 |
|---|---|---|
| 정해진 시간마다 도는 작업 | 요청이 없으면 안 돎 | k8s CronJob (기존 스크립트 재사용) |
| 오래 걸리는 작업 (큰 CSV) | 요청이 길게 붙잡혀 끊김 | 작업 큐 + 워커 |
| 무거운 계산 | 이벤트 루프를 막아 모두 멈춤 | worker_threads나 별도 서비스 |
떼어 내도 같은 저장소·같은 Docker 이미지에서 시작 명령만 바꿔 띄우는 경우가 많다. 화면과 BFF까지 버릴 필요는 없다.
- 체크리스트
- 비밀값이
NEXT_PUBLIC_(브라우저 공개 변수)에 들어가지 않았나. - 비밀값을 쓰는 파일에
server-only가 붙어 있나. - 관리자 API는 서버에서 역할을 확인하나(버튼 숨기기만으로는 부족).
- 문제가 생기면 먼저 볼 것: 브라우저 Network 탭에서 요청이 어디로 갔는지, CORS 오류가 떴는지, 상태 코드(401·403·502)가 무엇인지 본다.
❓ 자주 묻는 질문¶
- Q. 어떤 코드가 브라우저로 가는지 어떻게 아나?
A.
"use client"가 붙은 파일과 그 파일이 불러오는 것만 브라우저로 간다. 나머지는 서버에서 돈다. - Q. 서버끼리는 CORS가 없다면, 아무 서버나 매니저를 부를 수 있나? A. 요청은 갈 수 있다. 하지만 매니저가 토큰을 검사해서, 없으면 거절한다.
- Q. 화면 코드에 비밀값을 넣고 난독화하면 안 되나? A. 안 된다. 브라우저가 실행하려면 결국 값을 알아야 하므로 꺼낼 수 있다.
🔭 더 깊이 보기¶
- 세션 쿠키는 어떻게 "누구인지"를 기억하나? 로그인 뒤 서버가 사람을 알아보는 원리다.
- 이벤트 루프가 막힌다는 건 무슨 뜻인가? 무거운 계산을 왜 떼어 내야 하는지의 근거다.
- 관련 주제: BFF Backend For Frontend 패턴 — Next.js 서버 코드가 맡는 역할의 이름
- 관련 주제: 작업 큐와 백그라운드 워커 — 오래 걸리는 작업을 떼어 내는 방법
- 관련 주제: 서버 동시성 모델과 I/O 병목 — 단일 스레드와 무거운 계산
- 관련 주제: OIDC 로그인 흐름
📚 참고 자료¶
- 교차 출처 리소스 공유(CORS) (MDN 한국어) — 브라우저가 다른 주소 요청을 막고 허락하는 방식 · 링크
- 서버 측 웹사이트 프로그래밍 첫걸음 (MDN 한국어) — 서버 쪽 코드가 무엇을 하는지 입문 · 링크
- Next.js Docs (영문) — Server and Client Components, Route Handlers 항목 · 링크
🗓️ 이력¶
- 2026-11-09: 매니저가 있는데 서버가 왜 필요한지, Next.js로 서버 없이 되는지 물음 → 일지
- 2026-11-10: Next.js가 프론트·백을 함께 하는 이유와, 복잡해질 때 떼어 낼 부분을 물음 → 일지