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

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

이 글의 핵심

Express.js 완벽 가이드에 대한 실전 가이드입니다. Node.js 웹 프레임워크 등을 예제와 함께 상세히 설명합니다.

들어가며

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, 구조화대규모 엔터프라이즈

다음 단계

  • Node.js 파일 시스템
  • Node.js 데이터베이스 연동
  • Node.js 인증과 보안

추천 학습 자료

공식 문서:

미들웨어:


관련 글

  • TypeScript 실전 프로젝트 | REST API 서버 만들기
  • C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]
  • Node.js 시작하기 | 설치, 설정, Hello World
  • Flask 기초 | Python 웹 프레임워크 시작하기
  • Python REST API | Flask/Django로 API 서버 만들기