왜 이 글을 쓰나
백엔드를 혼자 개발하다가 팀 프로젝트에 합류하면 처음 맞닥뜨리는 불편함이 있다. API 엔드포인트가 뭔지, 요청 바디에 어떤 필드를 넣어야 하는지, 응답 구조가 어떻게 생겼는지를 팀원이 물어볼 때마다 구두로 설명하거나 슬랙에 타이핑해야 하는 상황이다.
노션이나 Confluence에 API 문서를 수동으로 작성해두면 코드가 바뀔 때마다 문서도 같이 수정해야 한다. 그걸 놓치면 문서와 실제 동작이 달라지고, 팀원 신뢰를 잃는다.
Swagger/OpenAPI를 쓰면 코드에서 직접 문서를 생성한다. 코드가 바뀌면 문서도 자동으로 바뀐다. 이 글은 Node.js/Express 기준으로 swagger-jsdoc + swagger-ui-express 설정부터 OpenAPI YAML 직접 작성, 2024-2025년에 급부상한 Scalar 도입까지 실전 설정을 정리한다.
OpenAPI vs Swagger — 개념 정리
이 둘을 같은 것으로 섞어 쓰는 경우가 많다. 정확히는 다르다.
| 용어 | 정의 |
|---|---|
| OpenAPI | REST API를 기술하는 표준 스펙. YAML/JSON 형식. 현재 3.0, 3.1 버전 |
| Swagger | SmartBear가 만든 OpenAPI 관련 툴셋. Swagger UI, Swagger Editor, Swagger Codegen 등 포함 |
| Swagger UI | OpenAPI 스펙을 브라우저에서 시각화하고 API를 직접 테스트할 수 있는 인터페이스 |
| swagger-jsdoc | JSDoc 주석에서 OpenAPI 스펙(JSON/YAML)을 자동 생성하는 Node.js 라이브러리 |
| swagger-ui-express | Express 앱에 Swagger UI를 미들웨어로 마운트하는 라이브러리 |
요약하면, OpenAPI는 스펙이고 Swagger는 그 스펙을 활용하는 툴셋이다. "Swagger 문서"라고 말하면 OpenAPI 스펙으로 작성된 문서를 의미하는 게 일반적이다.
Node.js/Express에서 swagger-jsdoc + swagger-ui-express 설정
설치
npm install swagger-jsdoc swagger-ui-express
TypeScript 환경이라면 타입 정의도 설치한다:
npm install -D @types/swagger-jsdoc @types/swagger-ui-express
기본 설정
프로젝트 루트에 swagger.js(또는 src/config/swagger.js)를 만든다:
// src/config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API 문서',
},
servers: [
{
url: 'http://localhost:3000',
description: '로컬 개발 서버',
},
{
url: 'https://api.example.com',
description: '프로덕션 서버',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
// JSDoc 주석이 있는 파일 경로 (glob 패턴)
apis: ['./src/routes/*.js', './src/models/*.js'],
};
const swaggerSpec = swaggerJsdoc(options);
module.exports = swaggerSpec;
app.js에서 Swagger UI를 마운트한다:
// app.js
const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./src/config/swagger');
const app = express();
// Swagger UI: /api-docs에서 접근
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// JSON 스펙 직접 접근: /api-docs.json
app.get('/api-docs.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swaggerSpec);
});
서버를 실행하고 http://localhost:3000/api-docs에 접속하면 Swagger UI가 보인다.
JSDoc 주석으로 API 문서 작성
GET — 목록 조회
// src/routes/users.js
/**
* @swagger
* /api/v1/users:
* get:
* summary: 사용자 목록 조회
* description: 페이지네이션을 지원하는 사용자 목록을 반환한다.
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 페이지 번호
* - in: query
* name: perPage
* schema:
* type: integer
* default: 20
* description: 페이지당 항목 수
* responses:
* 200:
* description: 조회 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
* meta:
* $ref: '#/components/schemas/PaginationMeta'
*/
router.get('/', async (req, res) => {
// ...
});
POST — 생성
/**
* @swagger
* /api/v1/users:
* post:
* summary: 사용자 생성
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateUserDto'
* responses:
* 201:
* description: 생성 성공
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* $ref: '#/components/schemas/User'
* 400:
* description: 유효성 검사 실패
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ValidationError'
*/
router.post('/', async (req, res) => {
// ...
});
PUT — 전체 수정
/**
* @swagger
* /api/v1/users/{id}:
* put:
* summary: 사용자 전체 수정
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 사용자 ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateUserDto'
* responses:
* 200:
* description: 수정 성공
* 404:
* description: 사용자를 찾을 수 없다
*/
router.put('/:id', async (req, res) => {
// ...
});
DELETE — 삭제
/**
* @swagger
* /api/v1/users/{id}:
* delete:
* summary: 사용자 삭제
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 사용자 ID
* responses:
* 204:
* description: 삭제 성공 (응답 바디 없음)
* 404:
* description: 사용자를 찾을 수 없다
*/
router.delete('/:id', async (req, res) => {
// ...
});
스키마 정의 — components/schemas
재사용 가능한 스키마를 별도 파일에 모아두면 관리가 편하다:
// src/models/user.schema.js
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* properties:
* id:
* type: integer
* example: 1
* name:
* type: string
* example: 김철수
* email:
* type: string
* format: email
* example: chulsoo@example.com
* createdAt:
* type: string
* format: date-time
* example: "2026-01-01T00:00:00Z"
*
* CreateUserDto:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* minLength: 2
* maxLength: 50
* example: 김철수
* email:
* type: string
* format: email
* example: chulsoo@example.com
* password:
* type: string
* minLength: 8
* example: securepassword123
*
* UpdateUserDto:
* type: object
* properties:
* name:
* type: string
* email:
* type: string
* format: email
*
* PaginationMeta:
* type: object
* properties:
* total:
* type: integer
* example: 100
* page:
* type: integer
* example: 1
* perPage:
* type: integer
* example: 20
* lastPage:
* type: integer
* example: 5
*
* ValidationError:
* type: object
* properties:
* error:
* type: object
* properties:
* code:
* type: string
* example: VALIDATION_ERROR
* message:
* type: string
* example: 입력값이 올바르지 않다
* details:
* type: array
* items:
* type: object
* properties:
* field:
* type: string
* message:
* type: string
*/
이 파일을 swagger.js의 apis 경로에 포함시키면 된다. $ref: '#/components/schemas/User'로 어디서든 참조 가능하다.
OpenAPI YAML 직접 작성 방법
JSDoc 주석 대신 별도의 YAML 파일로 스펙을 작성하는 방식이다. 코드와 문서를 분리하고 싶거나, 스펙을 먼저 설계하고 구현하는 "스펙 우선" 방식에 적합하다.
기본 구조
# openapi.yaml
openapi: 3.0.3
info:
title: My API
description: API 문서
version: 1.0.0
contact:
name: Luka
email: luka@example.com
servers:
- url: http://localhost:3000
description: 로컬 서버
- url: https://api.example.com
description: 프로덕션
tags:
- name: Users
description: 사용자 관련 API
paths:
/api/v1/users:
get:
summary: 사용자 목록 조회
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: perPage
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 조회 성공
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'
post:
summary: 사용자 생성
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserDto'
responses:
'201':
description: 생성 성공
'400':
description: 유효성 검사 실패
/api/v1/users/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: integer
get:
summary: 특정 사용자 조회
tags: [Users]
responses:
'200':
description: 조회 성공
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: 사용자를 찾을 수 없다
delete:
summary: 사용자 삭제
tags: [Users]
responses:
'204':
description: 삭제 성공
components:
schemas:
User:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: 김철수
email:
type: string
format: email
example: chulsoo@example.com
CreateUserDto:
type: object
required: [name, email, password]
properties:
name:
type: string
email:
type: string
format: email
password:
type: string
minLength: 8
PaginationMeta:
type: object
properties:
total:
type: integer
page:
type: integer
perPage:
type: integer
lastPage:
type: integer
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
이 YAML 파일을 Express에서 로드하는 방법:
const YAML = require('yamljs');
const swaggerDocument = YAML.load('./openapi.yaml');
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
yamljs 대신 js-yaml을 써도 된다:
const yaml = require('js-yaml');
const fs = require('fs');
const swaggerDocument = yaml.load(fs.readFileSync('./openapi.yaml', 'utf8'));
Scalar — Swagger UI의 대안
2024-2025년을 기점으로 Scalar가 빠르게 주목받고 있다. Swagger UI보다 UI가 훨씬 깔끔하고, 코드 예시 자동 생성, 다크모드, 반응형 레이아웃을 기본 제공한다.
Swagger UI vs Scalar 비교
| 항목 | Swagger UI | Scalar |
|---|---|---|
| UI 품질 | 기능 중심, 다소 투박함 | 모던하고 세련된 디자인 |
| 다크모드 | 기본 없음 (플러그인 필요) | 기본 제공 |
| 코드 예시 | 제한적 | curl, JS, Python, Go 등 자동 생성 |
| 반응형 | 부분적 | 완전 반응형 |
| Try it out | 있음 | 있음 |
| 번들 크기 | 상대적으로 큼 | 더 가벼움 |
| OpenAPI 버전 | 2.0, 3.0 지원 | 3.0, 3.1 지원 |
| 커스터마이징 | 제한적 | 테마, 레이아웃 유연 |
| 활성화 시점 | 오래된 표준 | 2023년 이후 급성장 |
Express에서 Scalar 설정
npm install @scalar/express-api-reference
const { apiReference } = require('@scalar/express-api-reference');
// swagger-jsdoc로 생성한 스펙 또는 YAML 파일 모두 사용 가능
app.use('/reference', apiReference({
spec: {
content: swaggerSpec, // swagger-jsdoc 결과물 or yamljs로 로드한 객체
},
theme: 'default', // 'default' | 'moon' | 'purple' | 'solarized' | 'bluePlanet'
}));
http://localhost:3000/reference로 접속하면 Scalar UI가 보인다. Swagger UI와 Scalar를 동시에 운영해도 된다. 팀 내 선호도에 따라 선택하면 된다.
Postman Collection 자동 생성
OpenAPI 스펙이 있으면 Postman Collection으로 변환할 수 있다. 팀에 Postman 사용자가 있다면 유용하다.
openapi-to-postmanv2 CLI
npm install -g openapi-to-postmanv2
# YAML 스펙에서 Postman Collection JSON 생성
openapi2postmanv2 -s openapi.yaml -o postman-collection.json
# 파일 없이 API 스펙 URL로 변환
openapi2postmanv2 -s http://localhost:3000/api-docs.json -o postman-collection.json
Postman UI에서 직접 가져오기
- Postman 실행
- Import 버튼 클릭
- Link 탭에서
http://localhost:3000/api-docs.json입력 - Import 클릭
생성된 Collection에 환경 변수({{baseUrl}})가 자동으로 적용된다.
문서 자동화 전략 비교
| 전략 | 방식 | 도구 | 장점 | 단점 |
|---|---|---|---|---|
| 코드 우선 (Code-First) | 코드 → 문서 자동 생성 | swagger-jsdoc | 코드와 문서 동기화 보장 | 주석이 길어져 코드 가독성 저하 |
| 스펙 우선 (Spec-First) | YAML/JSON 스펙 → 코드 구현 | OpenAPI YAML | API 설계 선행, 협업에 유리 | 코드 변경 시 수동 스펙 업데이트 필요 |
| 하이브리드 | 주요 엔드포인트는 YAML, 나머지는 JSDoc | 혼합 | 유연함 | 관리 포인트 분산 |
팀 규모와 개발 방식에 따라 다르다. 혼자 빠르게 만드는 프로젝트라면 코드 우선이 편하다. 팀이 있고 설계 단계에서 프론트엔드와 합의가 필요하다면 스펙 우선이 더 맞다.
삽질 기록
JSDoc 주석 위치 문제
swagger-jsdoc이 JSDoc 주석을 찾지 못해서 Swagger UI에 엔드포인트가 나타나지 않는 경우가 있다. 원인은 대부분 두 가지다.
첫째, apis 경로가 잘못됐다. 프로젝트 루트 기준 상대 경로를 써야 하는데 swagger.js 파일 기준으로 쓰는 실수를 했다.
// 잘못된 예: swagger.js가 src/config/에 있을 때
apis: ['../routes/*.js'] // swagger.js 기준 상대 경로 — 동작 안 함
// 올바른 예: 프로젝트 루트 기준
apis: ['./src/routes/*.js']
// 또는 절대 경로 사용
apis: [path.join(__dirname, '../../routes/*.js')]
둘째, 주석이 /** ... */ 형식이 아닌 경우다. /* ... */(별 하나)로 쓰면 인식 안 된다.
$ref 경로 오류
YAML에서 $ref를 쓸 때 경로 오타가 나면 Swagger UI에서 스키마가 렌더링되지 않고 빈 칸으로 표시된다.
# 잘못된 예
schema:
$ref: '#/components/schema/User' # schema (단수) — 존재하지 않는 경로
# 올바른 예
schema:
$ref: '#/components/schemas/User' # schemas (복수)
swagger-jsdoc를 쓸 때는 Node.js 콘솔에 에러 메시지가 출력된다. Swagger UI에서 UI가 이상하게 보이면 먼저 콘솔 로그를 확인하는 게 빠르다.
CORS 문제 — Swagger UI에서 Try it out이 안 된다
Swagger UI에서 API를 직접 호출할 때 CORS 에러가 나는 경우다. 로컬 개발 환경에서 Swagger UI(http://localhost:3000/api-docs)가 같은 오리진에서 API를 호출하는데도 CORS가 막히는 상황이 있었다.
원인은 Express CORS 설정에서 허용 오리진을 명시할 때 localhost:3000을 빠뜨린 경우였다.
const cors = require('cors');
app.use(cors({
origin: [
'http://localhost:3000',
'http://localhost:5173', // Vite 프론트엔드
'https://yourapp.com',
],
}));
프로덕션 환경에서 Swagger UI를 노출하는 경우에는 인증 미들웨어를 걸거나, 내부 IP 대역에서만 접근하도록 제한하는 것이 맞다. 프로덕션 API 문서가 외부에 노출되면 공격 표면이 된다.
마무리
Swagger/OpenAPI를 처음 설정하는 데는 시간이 조금 걸린다. JSDoc 주석을 처음부터 꼼꼼히 달거나, YAML 스펙을 처음 작성하는 작업이 귀찮게 느껴질 수 있다. 하지만 한 번 갖춰두면 팀원에게 API를 설명하는 시간이 사라지고, 프론트엔드 개발자가 알아서 문서를 보고 연동하기 시작한다.
Scalar는 Swagger UI보다 UI가 훨씬 좋다. 새 프로젝트를 시작한다면 Scalar를 기본으로 쓰는 것을 권장한다.
최소한 이것만 갖춰두면 된다:
swagger-jsdoc+swagger-ui-express(또는 Scalar) 설정- 각 엔드포인트에 JSDoc 주석 (summary, parameters, requestBody, responses)
components/schemas에 재사용 스키마 정의/api-docs또는/reference경로에 문서 노출
기술 스택: Node.js, Express, swagger-jsdoc, swagger-ui-express, OpenAPI 3.0, YAML, Scalar, Postman
'개발 팁' 카테고리의 다른 글
| SSH 터널링 & 포트 포워딩 — 원격 DB 접속부터 내부망 우회까지 (1) | 2026.03.20 |
|---|---|
| TypeScript 실전 팁 — 유틸리티 타입, 타입 가드, 제네릭 완전 정복 (0) | 2026.03.20 |
| cron 표현식 완전 정복 — 스케줄링 실전 치트시트 (1) | 2026.03.20 |
| npm vs yarn vs pnpm vs Bun — 패키지 매니저 제대로 선택하기 (1) | 2026.03.20 |
| Nginx 실전 설정 — 리버스 프록시, HTTPS, 캐싱 완전 정복 (0) | 2026.03.20 |