본문 바로가기
개발 팁

API 문서화 실전 — Swagger/OpenAPI로 자동 문서 만들기

by 루까(Luka) 2026. 3. 20.
반응형

왜 이 글을 쓰나

백엔드를 혼자 개발하다가 팀 프로젝트에 합류하면 처음 맞닥뜨리는 불편함이 있다. 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.jsapis 경로에 포함시키면 된다. $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에서 직접 가져오기

  1. Postman 실행
  2. Import 버튼 클릭
  3. Link 탭에서 http://localhost:3000/api-docs.json 입력
  4. 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

반응형