본문으로 건너뛰기
Previous
Next
Express.js 완벽 가이드 | Node.js 웹 프레임워크

Express.js 완벽 가이드 | Node.js 웹 프레임워크

Express.js 완벽 가이드 | Node.js 웹 프레임워크

이 글의 핵심

Express.js : Node.js 웹 프레임워크. 설치 및 기본 서버·라우팅 (Routing).

들어가며

Express.js란?

Express.js는 Node.js를 위한 빠르고 간결한 웹 프레임워크입니다. 요청이 들어오면 주소(경로)와 HTTP 메서드에 따라 교통 표지판·우편 집배처럼 “이 편지는 이 핸들러로”라고 연결해 줍니다. 그 사이사이에 미들웨어를 끼우는데, 이는 검문소나 공장의 필터처럼 로그인·JSON 파싱·CORS 같은 공통 작업을 통과시키는 단계라고 이해하시면 됩니다. 특징:

  • 간결한 API: 최소한의 코드로 서버 구축
  • 미들웨어: 요청 처리 파이프라인
  • 라우팅: URL 패턴 매칭
  • 템플릿 엔진: EJS, Pug 등 지원
  • 풍부한 생태계: 수천 개의 미들웨어 사용 사례:
  • REST API 서버
  • 웹 애플리케이션
  • 마이크로서비스
  • 프록시 서버

실전 경험에서 배운 교훈

이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.

가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.

이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.

1. 설치 및 기본 서버

설치

# 프로젝트 초기화
npm init -y
# Express 설치
npm install express
# 개발 도구 설치
npm install --save-dev nodemon

Hello World

// app.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
    res.send('Hello, Express!');
});
const PORT = 3000;
app.listen(PORT, () => {
    console.log(`서버 실행 중: http://localhost:${PORT}`);
});

실행:

node app.js
# 또는
nodemon app.js

기본 구조

const express = require('express');
const app = express();
// 미들웨어
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 라우트
app.get('/', (req, res) => {
    res.send('홈 페이지');
});
app.get('/about', (req, res) => {
    res.send('소개 페이지');
});
// 404 처리
app.use((req, res) => {
    res.status(404).send('페이지를 찾을 수 없습니다');
});
// 에러 처리
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('서버 에러');
});
// 서버 시작
app.listen(3000, () => {
    console.log('서버 실행 중: http://localhost:3000');
});

2. 라우팅 (Routing)

app.get('/users', …)처럼 경로 + 메서드가 맞을 때만 실행되는 함수를 붙입니다. 우편물 주소가 동네·번지·호수까지 정확히 맞아야 배달되듯, 클라이언트가 GET /api/users/42처럼 요청하면 :id42가 들어가 해당 핸들러 한 곳으로만 전달됩니다.

HTTP 메서드

const express = require('express');
const app = express();
// GET: 데이터 조회
app.get('/users', (req, res) => {
    res.json({ users: ['홍길동', '김철수'] });
});
// POST: 데이터 생성
app.post('/users', (req, res) => {
    const user = req.body;
    res.status(201).json({ message: '사용자 생성됨', user });
});
// PUT: 데이터 전체 수정
app.put('/users/:id', (req, res) => {
    const { id } = req.params;
    const user = req.body;
    res.json({ message: `사용자 ${id} 수정됨`, user });
});
// PATCH: 데이터 일부 수정
app.patch('/users/:id', (req, res) => {
    const { id } = req.params;
    const updates = req.body;
    res.json({ message: `사용자 ${id} 일부 수정됨`, updates });
});
// DELETE: 데이터 삭제
app.delete('/users/:id', (req, res) => {
    const { id } = req.params;
    res.json({ message: `사용자 ${id} 삭제됨` });
});
// ALL: 모든 메서드
app.all('/secret', (req, res) => {
    res.send('비밀 페이지');
});

경로 매개변수 (Route Parameters)

// 단일 매개변수
app.get('/users/:id', (req, res) => {
    const { id } = req.params;
    res.send(`사용자 ID: ${id}`);
});
// GET /users/123 → id: "123"
// 여러 매개변수
app.get('/users/:userId/posts/:postId', (req, res) => {
    const { userId, postId } = req.params;
    res.json({ userId, postId });
});
// GET /users/123/posts/456 → { userId: "123", postId: "456" }
// 정규식 패턴
app.get('/files/:filename(\\d+\\.txt)', (req, res) => {
    const { filename } = req.params;
    res.send(`파일: ${filename}`);
});
// GET /files/123.txt ✅
// GET /files/abc.txt ❌
// 선택적 매개변수
app.get('/users/:id?', (req, res) => {
    if (req.params.id) {
        res.send(`사용자 ID: ${req.params.id}`);
    } else {
        res.send('모든 사용자');
    }
});

쿼리 문자열 (Query String)

app.get('/search', (req, res) => {
    const { q, page, limit } = req.query;
    
    res.json({
        query: q,
        page: parseInt(page) || 1,
        limit: parseInt(limit) || 10
    });
});
// GET /search?q=nodejs&page=2&limit=20
// { query: "nodejs", page: 2, limit: 20 }

라우터 분리

// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
    res.json({ users: [] });
});
router.get('/:id', (req, res) => {
    res.json({ id: req.params.id });
});
router.post('/', (req, res) => {
    res.status(201).json({ message: '생성됨' });
});
module.exports = router;
// app.js
const express = require('express');
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');
const app = express();
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
app.listen(3000);

3. 미들웨어 (Middleware)

미들웨어란?

미들웨어는 요청과 응답 사이에서 실행되는 함수입니다. 들어온 요청이 라우트 핸들러에 닿기 전에 검문·신분 확인·본문(JSON) 해석 같은 공통 절차를 거치게 할 때 씁니다. next()를 호출해야만 다음 검문소로 통과하고, 호출하지 않으면 그 자리에서 응답으로 끝납니다.

function myMiddleware(req, res, next) {
    console.log('미들웨어 실행');
    next();  // 다음 미들웨어로 이동
}
app.use(myMiddleware);

실행 흐름:

요청 → 미들웨어1 → 미들웨어2 → 라우트 핸들러 → 응답

애플리케이션 레벨 미들웨어

const express = require('express');
const app = express();
// 모든 요청에 실행
app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    console.log('시간:', new Date().toISOString());
    next();
});
// 특정 경로에만 실행
app.use('/api', (req, res, next) => {
    console.log('API 요청');
    next();
});
// 여러 미들웨어 체이닝
app.get('/users',
    (req, res, next) => {
        console.log('미들웨어 1');
        next();
    },
    (req, res, next) => {
        console.log('미들웨어 2');
        next();
    },
    (req, res) => {
        res.send('사용자 목록');
    }
);

내장 미들웨어

// JSON 파싱
app.use(express.json());
// URL-encoded 파싱
app.use(express.urlencoded({ extended: true }));
// 정적 파일 서빙
app.use(express.static('public'));
// public/style.css → http://localhost:3000/style.css
// 여러 정적 폴더
app.use('/static', express.static('public'));
app.use('/uploads', express.static('uploads'));

서드파티 미들웨어

npm install cors morgan helmet compression
const cors = require('cors');
const morgan = require('morgan');
const helmet = require('helmet');
const compression = require('compression');
// CORS (Cross-Origin Resource Sharing)
app.use(cors());
// HTTP 요청 로깅
app.use(morgan('dev'));
// GET /users 200 15.234 ms - 123
// 보안 헤더
app.use(helmet());
// 응답 압축
app.use(compression());

커스텀 미들웨어

로깅 미들웨어:

function logger(req, res, next) {
    const start = Date.now();
    
    res.on('finish', () => {
        const duration = Date.now() - start;
        console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
    });
    
    next();
}
app.use(logger);

인증 미들웨어:

// 함수 정의 및 구현
function authenticate(req, res, next) {
    const token = req.headers.authorization;
    
    if (!token) {
        return res.status(401).json({ error: '인증 토큰이 필요합니다' });
    }
    
    try {
        // 토큰 검증 (예: JWT)
        const user = verifyToken(token);
        req.user = user;
        next();
    } catch (err) {
        res.status(401).json({ error: '유효하지 않은 토큰' });
    }
}
// 보호된 라우트
app.get('/profile', authenticate, (req, res) => {
    res.json({ user: req.user });
});

권한 확인 미들웨어:

function authorize(...roles) {
    return (req, res, next) => {
        if (!req.user) {
            return res.status(401).json({ error: '인증 필요' });
        }
        
        if (!roles.includes(req.user.role)) {
            return res.status(403).json({ error: '권한 없음' });
        }
        
        next();
    };
}
// 사용
app.delete('/users/:id', 
    authenticate,
    authorize('admin'),
    (req, res) => {
        res.json({ message: '사용자 삭제됨' });
    }
);

4. REST API 구축

CRUD 구현

const express = require('express');
const app = express();
app.use(express.json());
// 임시 데이터베이스
let users = [
    { id: 1, name: '홍길동', email: '[email protected]' },
    { id: 2, name: '김철수', email: '[email protected]' }
];
let nextId = 3;
// CREATE: 사용자 생성
app.post('/api/users', (req, res) => {
    const { name, email } = req.body;
    
    // 유효성 검사
    if (!name || !email) {
        return res.status(400).json({ error: '이름과 이메일이 필요합니다' });
    }
    
    const user = { id: nextId++, name, email };
    users.push(user);
    
    res.status(201).json(user);
});
// READ: 모든 사용자 조회
app.get('/api/users', (req, res) => {
    const { page = 1, limit = 10 } = req.query;
    
    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;
    
    const paginatedUsers = users.slice(startIndex, endIndex);
    
    res.json({
        users: paginatedUsers,
        total: users.length,
        page: parseInt(page),
        totalPages: Math.ceil(users.length / limit)
    });
});
// READ: 특정 사용자 조회
app.get('/api/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const user = users.find(u => u.id === id);
    
    if (!user) {
        return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
    }
    
    res.json(user);
});
// UPDATE: 사용자 수정
app.put('/api/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const { name, email } = req.body;
    
    const userIndex = users.findIndex(u => u.id === id);
    
    if (userIndex === -1) {
        return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
    }
    
    users[userIndex] = { id, name, email };
    res.json(users[userIndex]);
});
// DELETE: 사용자 삭제
app.delete('/api/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const userIndex = users.findIndex(u => u.id === id);
    
    if (userIndex === -1) {
        return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
    }
    
    users.splice(userIndex, 1);
    res.status(204).send();  // No Content
});
app.listen(3000);

HTTP 상태 코드

코드의미사용 예
200OK성공
201Created리소스 생성
204No Content삭제 성공
400Bad Request잘못된 요청
401Unauthorized인증 필요
403Forbidden권한 없음
404Not Found리소스 없음
500Internal Server Error서버 에러

5. 요청과 응답

Request 객체

app.get('/demo', (req, res) => {
    // URL 매개변수
    console.log(req.params);
    
    // 쿼리 문자열
    console.log(req.query);
    
    // 요청 본문
    console.log(req.body);
    
    // 헤더
    console.log(req.headers);
    console.log(req.get('User-Agent'));
    
    // HTTP 메서드
    console.log(req.method);
    
    // URL
    console.log(req.url);
    console.log(req.path);
    console.log(req.originalUrl);
    
    // IP 주소
    console.log(req.ip);
    
    // 쿠키 (cookie-parser 필요)
    console.log(req.cookies);
    
    res.send('완료');
});

Response 객체

app.get('/response-demo', (req, res) => {
    // 텍스트 응답
    res.send('Hello');
    
    // JSON 응답
    res.json({ message: 'Success' });
    
    // 상태 코드 + JSON
    res.status(201).json({ created: true });
    
    // 파일 전송
    res.sendFile('/path/to/file.pdf');
    
    // 파일 다운로드
    res.download('/path/to/file.pdf', 'custom-name.pdf');
    
    // 리다이렉트
    res.redirect('/new-url');
    res.redirect(301, '/permanent-url');
    
    // 헤더 설정
    res.set('Content-Type', 'text/html');
    res.set({
        'Content-Type': 'application/json',
        'X-Custom-Header': 'value'
    });
    
    // 쿠키 설정
    res.cookie('name', 'value', { maxAge: 900000, httpOnly: true });
    
    // 쿠키 삭제
    res.clearCookie('name');
    
    // 렌더링 (템플릿 엔진)
    res.render('index', { title: '홈' });
});

6. 미들웨어 심화

에러 처리 미들웨어

// 비동기 에러 래퍼
function asyncHandler(fn) {
    return (req, res, next) => {
        Promise.resolve(fn(req, res, next)).catch(next);
    };
}
// 사용
app.get('/users/:id', asyncHandler(async (req, res) => {
    const user = await User.findById(req.params.id);
    
    if (!user) {
        throw new Error('사용자를 찾을 수 없습니다');
    }
    
    res.json(user);
}));
// 에러 처리 미들웨어 (맨 마지막에 배치)
app.use((err, req, res, next) => {
    console.error(err.stack);
    
    const statusCode = err.statusCode || 500;
    const message = err.message || '서버 에러';
    
    res.status(statusCode).json({
        error: {
            message,
            ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
        }
    });
});

커스텀 에러 클래스

// errors.js
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
    }
}
class NotFoundError extends AppError {
    constructor(message = '리소스를 찾을 수 없습니다') {
        super(message, 404);
    }
}
class ValidationError extends AppError {
    constructor(message = '유효하지 않은 입력') {
        super(message, 400);
    }
}
module.exports = { AppError, NotFoundError, ValidationError };
// 사용
const { NotFoundError, ValidationError } = require('./errors');
app.get('/users/:id', async (req, res, next) => {
    try {
        const user = await User.findById(req.params.id);
        
        if (!user) {
            throw new NotFoundError('사용자를 찾을 수 없습니다');
        }
        
        res.json(user);
    } catch (err) {
        next(err);
    }
});

7. 실전 예제

예제 1: 블로그 API

// app.js
const express = require('express');
const app = express();
app.use(express.json());
// 데이터
let posts = [
    { id: 1, title: '첫 글', content: '내용', author: '홍길동', createdAt: new Date() }
];
let nextId = 2;
// 모든 글 조회
app.get('/api/posts', (req, res) => {
    const { author, sort = 'desc' } = req.query;
    
    let result = [...posts];
    
    // 필터링
    if (author) {
        result = result.filter(p => p.author === author);
    }
    
    // 정렬
    result.sort((a, b) => {
        return sort === 'asc' 
            ? a.createdAt - b.createdAt 
            : b.createdAt - a.createdAt;
    });
    
    res.json({ posts: result, total: result.length });
});
// 특정 글 조회
app.get('/api/posts/:id', (req, res) => {
    const post = posts.find(p => p.id === parseInt(req.params.id));
    
    if (!post) {
        return res.status(404).json({ error: '글을 찾을 수 없습니다' });
    }
    
    res.json(post);
});
// 글 작성
app.post('/api/posts', (req, res) => {
    const { title, content, author } = req.body;
    
    if (!title || !content || !author) {
        return res.status(400).json({ 
            error: '제목, 내용, 작성자가 필요합니다' 
        });
    }
    
    const post = {
        id: nextId++,
        title,
        content,
        author,
        createdAt: new Date()
    };
    
    posts.push(post);
    res.status(201).json(post);
});
// 글 수정
app.put('/api/posts/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const { title, content } = req.body;
    
    const postIndex = posts.findIndex(p => p.id === id);
    
    if (postIndex === -1) {
        return res.status(404).json({ error: '글을 찾을 수 없습니다' });
    }
    
    posts[postIndex] = {
        ...posts[postIndex],
        title,
        content,
        updatedAt: new Date()
    };
    
    res.json(posts[postIndex]);
});
// 글 삭제
app.delete('/api/posts/:id', (req, res) => {
    const id = parseInt(req.params.id);
    const postIndex = posts.findIndex(p => p.id === id);
    
    if (postIndex === -1) {
        return res.status(404).json({ error: '글을 찾을 수 없습니다' });
    }
    
    posts.splice(postIndex, 1);
    res.status(204).send();
});
app.listen(3000, () => {
    console.log('블로그 API 서버 실행 중: http://localhost:3000');
});

예제 2: 파일 업로드

npm install multer
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 저장 설정
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/');
    },
    filename: (req, file, cb) => {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
        cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
    }
});
// 파일 필터
const fileFilter = (req, file, cb) => {
    const allowedTypes = /jpeg|jpg|png|gif/;
    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = allowedTypes.test(file.mimetype);
    
    if (extname && mimetype) {
        cb(null, true);
    } else {
        cb(new Error('이미지 파일만 업로드 가능합니다'));
    }
};
const upload = multer({
    storage,
    fileFilter,
    limits: { fileSize: 5 * 1024 * 1024 }  // 5MB
});
// 단일 파일 업로드
app.post('/upload', upload.single('image'), (req, res) => {
    if (!req.file) {
        return res.status(400).json({ error: '파일이 필요합니다' });
    }
    
    res.json({
        message: '업로드 성공',
        file: {
            filename: req.file.filename,
            size: req.file.size,
            path: req.file.path
        }
    });
});
// 여러 파일 업로드
app.post('/upload-multiple', upload.array('images', 5), (req, res) => {
    if (!req.files || req.files.length === 0) {
        return res.status(400).json({ error: '파일이 필요합니다' });
    }
    
    res.json({
        message: `${req.files.length}개 파일 업로드 성공`,
        files: req.files.map(f => ({
            filename: f.filename,
            size: f.size
        }))
    });
});
// 에러 처리
app.use((err, req, res, next) => {
    if (err instanceof multer.MulterError) {
        if (err.code === 'LIMIT_FILE_SIZE') {
            return res.status(400).json({ error: '파일 크기가 너무 큽니다' });
        }
    }
    
    res.status(500).json({ error: err.message });
});
app.listen(3000);

예제 3: 인증 시스템

npm install bcrypt jsonwebtoken
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = 'your-secret-key';
const users = [];
// 회원가입
app.post('/auth/register', async (req, res) => {
    try {
        const { email, password, name } = req.body;
        
        // 유효성 검사
        if (!email || !password || !name) {
            return res.status(400).json({ error: '모든 필드가 필요합니다' });
        }
        
        // 중복 확인
        if (users.find(u => u.email === email)) {
            return res.status(400).json({ error: '이미 존재하는 이메일입니다' });
        }
        
        // 비밀번호 해싱
        const hashedPassword = await bcrypt.hash(password, 10);
        
        const user = {
            id: users.length + 1,
            email,
            password: hashedPassword,
            name,
            createdAt: new Date()
        };
        
        users.push(user);
        
        // 비밀번호 제외하고 응답
        const { password: _, ...userWithoutPassword } = user;
        res.status(201).json(userWithoutPassword);
        
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});
// 로그인
app.post('/auth/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        
        // 사용자 찾기
        const user = users.find(u => u.email === email);
        
        if (!user) {
            return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
        }
        
        // 비밀번호 확인
        const isValid = await bcrypt.compare(password, user.password);
        
        if (!isValid) {
            return res.status(401).json({ error: '이메일 또는 비밀번호가 잘못되었습니다' });
        }
        
        // JWT 토큰 생성
        const token = jwt.sign(
            { id: user.id, email: user.email },
            JWT_SECRET,
            { expiresIn: '1h' }
        );
        
        res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
        
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});
// 인증 미들웨어
function authenticate(req, res, next) {
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: '인증 토큰이 필요합니다' });
    }
    
    const token = authHeader.substring(7);
    
    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        res.status(401).json({ error: '유효하지 않은 토큰' });
    }
}
// 보호된 라우트
app.get('/auth/profile', authenticate, (req, res) => {
    const user = users.find(u => u.id === req.user.id);
    
    if (!user) {
        return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
    }
    
    const { password, ...userWithoutPassword } = user;
    res.json(userWithoutPassword);
});
app.listen(3000);

8. 템플릿 엔진 (EJS)

설치 및 설정

npm install ejs
const express = require('express');
const app = express();
// 뷰 엔진 설정
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
    res.render('index', {
        title: '홈 페이지',
        message: 'Express + EJS'
    });
});
app.listen(3000);

EJS 템플릿

<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= message %></h1>
    
    <% if (users && users.length > 0) { %>
        <ul>
            <% users.forEach(user => { %>
                <li><%= user.name %> (<%= user.email %>)</li>
            <% }); %>
        </ul>
    <% } else { %>
        <p>사용자가 없습니다.</p>
    <% } %>
</body>
</html>
app.get('/users', (req, res) => {
    const users = [
        { name: '홍길동', email: '[email protected]' },
        { name: '김철수', email: '[email protected]' }
    ];
    
    res.render('index', {
        title: '사용자 목록',
        message: '등록된 사용자',
        users
    });
});

9. 보안

기본 보안 설정

npm install helmet cors express-rate-limit
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
// Helmet: 보안 헤더 설정
app.use(helmet());
// CORS 설정
app.use(cors({
    origin: 'https://yourdomain.com',
    credentials: true
}));
// Rate Limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,  // 15분
    max: 100,  // 최대 100개 요청
    message: '너무 많은 요청이 발생했습니다'
});
app.use('/api/', limiter);
// XSS 방지
app.use(express.json({ limit: '10kb' }));
// SQL Injection 방지 (파라미터 검증)
app.get('/users/:id', (req, res) => {
    const id = parseInt(req.params.id);
    
    if (isNaN(id)) {
        return res.status(400).json({ error: '유효하지 않은 ID' });
    }
    
    // ...
});
app.listen(3000);

입력 검증

npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/api/users',
    // 검증 규칙
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    body('name').trim().notEmpty(),
    
    // 핸들러
    (req, res) => {
        const errors = validationResult(req);
        
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        
        // 유효한 데이터 처리
        res.json({ message: '사용자 생성됨' });
    }
);

10. 프로덕션 배포

환경 설정

// config.js
module.exports = {
    port: process.env.PORT || 3000,
    nodeEnv: process.env.NODE_ENV || 'development',
    isDevelopment: process.env.NODE_ENV === 'development',
    isProduction: process.env.NODE_ENV === 'production'
};

프로덕션 설정

const express = require('express');
const config = require('./config');
const app = express();
// 프로덕션 전용 설정
if (config.isProduction) {
    // 신뢰할 수 있는 프록시 설정
    app.set('trust proxy', 1);
    
    // 압축
    const compression = require('compression');
    app.use(compression());
    
    // 로깅
    const morgan = require('morgan');
    app.use(morgan('combined'));
}
// 개발 전용 설정
if (config.isDevelopment) {
    const morgan = require('morgan');
    app.use(morgan('dev'));
}
app.listen(config.port);

PM2로 배포

# PM2 설치
npm install -g pm2
# 앱 시작
pm2 start app.js --name "my-app"
# 클러스터 모드 (멀티 코어 활용)
pm2 start app.js -i max
# 상태 확인
pm2 status
pm2 logs
pm2 monit
# 재시작
pm2 restart my-app
# 중지
pm2 stop my-app
# 삭제
pm2 delete my-app
# 부팅 시 자동 시작
pm2 startup
pm2 save

Nginx 리버스 프록시

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name yourdomain.com;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

11. 자주 발생하는 문제

문제 1: Cannot set headers after they are sent

원인: 응답을 두 번 보냄

// ❌ 잘못된 코드
app.get('/users/:id', (req, res) => {
    const user = users.find(u => u.id === parseInt(req.params.id));
    
    if (!user) {
        res.status(404).json({ error: '없음' });
    }
    
    res.json(user);  // 에러! (404 응답 후 또 응답)
});
// ✅ return 사용
app.get('/users/:id', (req, res) => {
    const user = users.find(u => u.id === parseInt(req.params.id));
    
    if (!user) {
        return res.status(404).json({ error: '없음' });
    }
    
    res.json(user);
});

문제 2: 미들웨어 순서

// ❌ 잘못된 순서
app.get('/users', (req, res) => {
    res.json({ users: [] });
});
app.use(express.json());  // 너무 늦음!
// ✅ 올바른 순서
app.use(express.json());  // 먼저
app.get('/users', (req, res) => {
    console.log(req.body);  // 파싱됨
    res.json({ users: [] });
});

문제 3: next() 호출 누락

// ❌ next() 없음
app.use((req, res, next) => {
    console.log('미들웨어');
    // next()를 호출하지 않으면 여기서 멈춤
});
// ✅ next() 호출
app.use((req, res, next) => {
    console.log('미들웨어');
    next();  // 다음으로 진행
});

12. 실전 팁

프로젝트 구조

my-express-app/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── controllers/
│   │   ├── userController.js
│   │   └── postController.js
│   ├── middlewares/
│   │   ├── auth.js
│   │   └── errorHandler.js
│   ├── models/
│   │   ├── User.js
│   │   └── Post.js
│   ├── routes/
│   │   ├── userRoutes.js
│   │   └── postRoutes.js
│   ├── utils/
│   │   └── logger.js
│   └── app.js
├── public/
├── views/
├── .env
├── .gitignore
├── package.json
└── server.js

컨트롤러 패턴

// controllers/userController.js
const User = require('../models/User');
exports.getAllUsers = async (req, res, next) => {
    try {
        const users = await User.find();
        res.json({ users });
    } catch (err) {
        next(err);
    }
};
exports.getUserById = async (req, res, next) => {
    try {
        const user = await User.findById(req.params.id);
        
        if (!user) {
            return res.status(404).json({ error: '사용자 없음' });
        }
        
        res.json(user);
    } catch (err) {
        next(err);
    }
};
exports.createUser = async (req, res, next) => {
    try {
        const user = await User.create(req.body);
        res.status(201).json(user);
    } catch (err) {
        next(err);
    }
};
// routes/userRoutes.js
// 변수 선언 및 초기화
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
module.exports = router;
// app.js
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);

API 버전 관리

// v1/routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
    res.json({ version: 'v1', users: [] });
});
module.exports = router;
// v2/routes/users.js
// 변수 선언 및 초기화
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
    res.json({ version: 'v2', users: [], meta: {} });
});
module.exports = router;
// app.js
// 변수 선언 및 초기화
const v1Users = require('./v1/routes/users');
const v2Users = require('./v2/routes/users');
app.use('/api/v1/users', v1Users);
app.use('/api/v2/users', v2Users);

정리

핵심 요약

  1. Express.js: Node.js 웹 프레임워크
  2. 라우팅: HTTP 메서드 + URL 패턴
  3. 미들웨어: 요청 처리 파이프라인
  4. REST API: CRUD 작업 구현
  5. 에러 처리: 에러 미들웨어, 비동기 래퍼
  6. 보안: Helmet, CORS, Rate Limiting
  7. 배포: PM2, Nginx

Express vs 다른 프레임워크

프레임워크특징사용 시기
Express간결, 생태계 큼일반적인 웹 앱
Fastify빠름, 스키마 검증고성능 API
Koa최신 문법, 가벼움모던 프로젝트
NestJSTypeScript, 구조화대규모 엔터프라이즈

다음 단계

추천 학습 자료

공식 문서:


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Express.js 완벽 가이드 | Node.js 웹 프레임워크」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Express.js 완벽 가이드 | Node.js 웹 프레임워크」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Express.js 완벽 가이드: Node.js 웹 프레임워크. 설치 및 기본 서버·라우팅 (Routing)로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Node.js·Express·웹프레임워크 중심으로… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, Express, 웹프레임워크, REST API, 미들웨어, 백엔드 등으로 검색하시면 이 글이 도움이 됩니다.