Vercel로 API를 만들다 보면 두 가지 구조 중 하나를 선택하게 됩니다.
- api/attendance/status.ts 같은 Vercel Function 파일
- routes/attendance.ts 같은 Express 라우터
둘 다 같은 URL로 접근하고, 같은 일을 합니다. 그런데 왜 Express를 쓰는 게 더 낫다고 하는 걸까요?
이번 포스팅에서 음식점 비유와 실제 코드로 명확하게 정리해 드릴게요.
핵심 차이 한 문장으로
Vercel Function = 주문마다 주방을 새로 차리는 것
Express 미들웨어 = 이미 열려 있는 식당에서 요리만 하는 것

Vercel Function 구조 — 파일 하나가 서버 하나
Vercel Function은 파일 경로가 곧 URL 경로입니다.
api/attendance/status.ts → GET /api/attendance/status
api/attendance/check.ts → POST /api/attendance/check
api/lottery/draw.ts → POST /api/lottery/draw
요청이 오면 Vercel이 해당 파일을 Lambda로 띄워서 실행하고, 끝나면 종료합니다.
문제는 여기서 시작됩니다. 각 파일이 독립적으로 실행되기 때문에, DB 연결과 auth 체크를 매 파일마다 직접 해줘야 합니다.
// api/attendance/status.ts
export default async function handler(req, res) {
// DB 연결 (매 요청마다 새로)
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
)
// auth 체크 (직접)
const token = req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: 'Unauthorized' })
const user = jwt.verify(token, process.env.JWT_SECRET!)
if (!user) return res.status(401).json({ error: 'Unauthorized' })
// 겨우 여기서부터 실제 로직
const { data } = await supabase
.from('attendance')
.select('*')
.eq('user_id', user.id)
res.json(data)
}
파일이 10개라면 이 auth 체크 코드를 10군데 다 써야 합니다. 복붙이죠.
그리고 이게 바로 버그의 시작입니다.
api/attendance/status.ts → auth 체크 ✅
api/attendance/check.ts → auth 체크 ✅
api/lottery/draw.ts → auth 체크 ✅
api/lottery/status.ts → auth 체크 ❌ ← 하나 빠뜨림 → 401 에러
파일 하나에서 빠뜨리는 순간 401 Unauthorized 에러가 납니다. 빠르게 개발하다 보면 반드시 실수가 납니다.
Express 미들웨어 구조 — 검문소를 한 번만 세운다
Express는 구조가 다릅니다. 하나의 서버 프로세스가 항상 떠 있고, 모든 요청이 그 안을 통과합니다.
요청 → Express 앱 → 미들웨어 → 라우터 → 응답
미들웨어는 모든 요청이 반드시 거쳐가는 검문소입니다. 한 번만 세워두면 모든 라우트에 자동으로 적용됩니다.
// index.ts
const app = express()
// 미들웨어 한 번만 등록하면 끝
app.use(authMiddleware) // 모든 요청에 auth 자동 적용
app.use(loggingMiddleware) // 모든 요청에 로그 자동 기록
app.use('/api', router)
// middleware/auth.ts
export const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: 'Unauthorized' })
req.user = jwt.verify(token, process.env.JWT_SECRET!)
next() // 통과하면 다음으로
}
이제 각 라우터는 실제 로직만 작성하면 됩니다.
// routes/attendance.ts
router.get('/status', async (req, res) => {
// auth는 이미 통과됨
// req.user 바로 사용 가능
const { data } = await supabase
.from('attendance')
.select('*')
.eq('user_id', req.user.id)
res.json(data)
})
auth 체크 코드가 라우터에 한 줄도 없습니다. 빠뜨릴 수가 없는 구조입니다.
정리 비교
Vercel Function Express 미들웨어
| 실행 방식 | 요청마다 새로 시작 | 하나의 서버가 항상 실행 중 |
| auth 체크 | 파일마다 직접 작성 | 미들웨어 한 번 등록으로 전체 적용 |
| DB 연결 | 매 요청마다 새로 연결 | 앱 시작 시 한 번 연결 |
| 실수 가능성 | 파일 하나라도 빠뜨리면 버그 | 빠뜨릴 수가 없음 |
| 코드 중복 | 파일마다 중복 | 없음 |
그럼 미들웨어가 하는 일은?
미들웨어를 한 마디로 정리하면 이렇습니다.
모든 요청이 실제 처리되기 전에 공통으로 실행되는 코드
주로 이런 일들을 담당합니다.
- auth — 토큰 검증, 사용자 확인
- logging — 요청 URL, 응답 시간 기록
- rate limiting — 초당 요청 수 제한
- cors — 허용된 도메인 체크
- body parsing — JSON 파싱
그리고 DB 연결은 미들웨어가 아니라 앱이 시작될 때 한 번 맺어 두는 겁니다. 미들웨어는 요청마다 실행되는 체크포인트고, DB 연결은 앱 자체에 미리 깔려 있는 수도관 같은 개념이에요.
왜 Vercel Function 구조로 시작하게 됐을까?
Vercel 튜토리얼과 Next.js의 기본 구조가 Function 방식입니다. 빠르게 시작하기엔 편하지만, 파일이 늘어날수록 공통 로직 중복이 쌓이기 시작합니다.
특히 빠른 속도로 여러 앱을 만들다 보면 이 문제가 더 두드러집니다. 처음엔 괜찮아 보이지만, 나중에 auth 체크 하나 빠진 파일에서 반드시 에러가 납니다.
결론
작은 앱이라면 Vercel Function도 충분합니다.
하지만 auth, 로깅, rate limit 같은 공통 처리가 필요한 앱이라면 Express 미들웨어 구조가 훨씬 안전합니다.
실수를 줄이는 가장 좋은 방법은 실수할 수 없는 구조를 만드는 것이니까요.
'Tech Notes' 카테고리의 다른 글
| 고양이 타로 앱에 서버를 안 두기로 했다 — Edge Function 도입기 (0) | 2026.05.11 |
|---|---|
| [앱인토스] 토스 로그인과 JWT, 14일의 비밀 (0) | 2026.04.26 |
| 폴백(Fallback)이란? (0) | 2026.04.25 |
| Vercel 무료 플랜 limit? (0) | 2026.04.16 |
| vercel 의 speedInsignts? (0) | 2026.04.15 |