정적 파일 서비스와 에러 처리
이 토픽을 마치면
Express에서 HTML, CSS, 이미지 같은 정적 파일을 서비스할 수 있고, 404(페이지 없음)와 500(서버 오류) 에러를 구조적으로 처리할 수 있습니다.
정적 파일이란
웹 서버가 처리하는 파일은 두 종류입니다.
- 동적 파일: 요청마다 서버가 코드를 실행해서 만드는 응답 (API, 데이터베이스 조회 결과)
- 정적 파일: 서버가 그대로 전달만 하는 파일 (HTML, CSS, JavaScript, 이미지, 폰트)
블로그의 글 목록은 동적이지만, 블로그의 로고 이미지와 스타일시트는 정적입니다. 정적 파일은 요청이 올 때마다 새로 만들 필요가 없으므로, 별도의 미들웨어가 효율적으로 처리합니다.
express.static — 정적 파일 미들웨어
Express는 express.static() 미들웨어를 내장하고 있습니다.
project/
├── app.js
├── public/
│ ├── index.html
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ └── images/
│ └── logo.pngconst express = require('express');
const path = require('path');
const app = express();
// public 폴더를 정적 파일 루트로 설정
app.use(express.static(path.join(__dirname, 'public')));
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});이제 브라우저에서 http://localhost:3000/css/style.css로 접속하면 public/css/style.css 파일이 반환됩니다. URL에 /public을 붙이지 않아도 됩니다 — express.static이 public 폴더를 루트로 매핑하기 때문입니다.
URL 접두사 추가
// /assets/css/style.css 로 접근
app.use('/assets', express.static(path.join(__dirname, 'public')));첫 번째 인자가 URL 접두사입니다. 이렇게 하면 실제 파일 경로는 public/css/style.css이지만, URL은 /assets/css/style.css가 됩니다. CDN이나 버저닝을 위해 경로를 분리할 때 유용합니다.
여러 폴더 지정
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'uploads')));두 폴더를 모두 정적 파일 소스로 등록할 수 있습니다. 첫 번째 폴더에서 못 찾으면 두 번째를 탐색합니다. 순서가 우선순위입니다.
path.join과 __dirname — 경로 안전하게 만들기
// Bad: 상대경로 — 실행 위치에 따라 달라짐
app.use(express.static('public'));
// Good: 절대경로 — 어디서 실행해도 동일
app.use(express.static(path.join(__dirname, 'public')));__dirname은 현재 파일이 있는 디렉토리의 절대경로입니다. path.join()은 OS에 맞는 경로 구분자(/ 또는 \)를 자동으로 처리합니다. Windows에서 /를 직접 쓰면 문제가 생길 수 있으므로, 항상 path.join을 사용합니다.
404 처리 — 페이지를 찾을 수 없음
Express에서 어떤 라우트에도 매칭되지 않으면, 요청은 그냥 사라집니다. 클라이언트는 응답을 영원히 기다리게 됩니다. 이것을 방지하려면 모든 라우트 뒤에 404 핸들러를 넣어야 합니다.
// 라우트 정의
app.get('/', (req, res) => {
res.send('Home');
});
app.get('/about', (req, res) => {
res.send('About');
});
// 404 핸들러 — 모든 라우트 뒤, 에러 핸들러 앞
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `${req.method} ${req.url} does not exist`,
status: 404
});
});위치가 중요합니다. app.use()는 순서대로 실행되므로, 404 핸들러를 맨 앞에 놓으면 모든 요청이 404가 됩니다. 반드시 모든 라우트 정의 뒤에 놓아야 합니다.
HTML 404 페이지
API 서버가 아니라 웹사이트라면, JSON 대신 HTML 페이지를 보여주는 것이 자연스럽습니다.
app.use((req, res) => {
res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
});500 처리 — 서버 내부 오류
에러 처리 미들웨어는 파라미터가 4개입니다. Express는 이 시그니처로 일반 미들웨어와 에러 핸들러를 구분합니다.
// 에러 핸들러 — 404 핸들러보다 뒤에
app.use((err, req, res, next) => {
console.error(`[ERROR] ${err.stack}`);
res.status(err.status || 500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message,
status: err.status || 500
});
});주의할 점 두 가지:
-
프로덕션에서는 에러 메시지를 숨깁니다.
err.message에 SQL 쿼리나 파일 경로 같은 민감한 정보가 들어있을 수 있기 때문입니다. 개발 환경에서만 상세 메시지를 보여줍니다. -
에러를 던지는 방법: 라우트 안에서
next(err)를 호출하면 에러 핸들러로 점프합니다.
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
const err = new Error('User not found');
err.status = 404;
return next(err);
}
res.json(user);
} catch (err) {
next(err); // DB 에러 등 예상치 못한 오류
}
});전체 구조 — 올바른 순서
const express = require('express');
const path = require('path');
const app = express();
// 1. 기본 미들웨어
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// 2. 라우트
app.get('/api/users', (req, res) => { /* ... */ });
app.post('/api/users', (req, res) => { /* ... */ });
// 3. 404 핸들러 (라우트 뒤)
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// 4. 에러 핸들러 (항상 마지막)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(3000);이 순서는 Express 프로젝트의 표준 구조입니다. 기본 미들웨어 → 라우트 → 404 → 에러. 이 순서를 어기면 예상치 못한 동작이 발생합니다.
실전에서 자주 하는 실수
| 실수 | 증상 | 해결 |
|---|---|---|
| 404 핸들러가 라우트보다 앞 | 모든 요청이 404 | 404를 맨 뒤로 이동 |
| 에러 핸들러 파라미터 3개 | 에러가 잡히지 않음 | (err, req, res, next) 4개 필수 |
express.static 경로가 상대 | 다른 폴더에서 실행하면 파일 못 찾음 | path.join(__dirname, ...) |
에러에서 next(err) 안 호출 | 에러 핸들러 도달 불가 | try/catch + next(err) |
프로덕션에서 err.stack 노출 | 보안 취약점 | NODE_ENV 분기 |
핵심 정리
정적 파일과 에러 처리는 "서버가 정상일 때"와 "비정상일 때"를 모두 다루는 것입니다. express.static은 파일을 효율적으로 서비스하고, 404/500 핸들러는 예외 상황에서도 사용자에게 의미 있는 응답을 보장합니다. Express의 미들웨어 순서(파싱 → 정적 → 라우트 → 404 → 에러)를 기억하면, 어디에 무엇을 놓아야 하는지 헷갈리지 않습니다.