서버는 돌아가는데, 이미지가 안 보인다?
웹 프로젝트를 진행하다 보면 한 번쯤 이런 현상을 겪게 된다.
“이미지 업로드는 잘 되는데, 브라우저에서 새로 업로드한 이미지를 불러오지 못한다.”
서버를 재시작하면 잘 되는데, 서버가 계속 실행 중일 때는 새로 추가된 이미지가 인식되지 않는 경우도 있다.
이 현상은 대부분 정적 파일 서빙(static file serving) 문제이거나 경로 설정(path configuration) 문제에서 비롯된다.
1. 서버 재시작 전까지 이미지를 불러오지 못하는 경우
서버 코드가 업로드된 파일을 즉시 반영하지 못하는 경우다.
예를 들어 Express 기반 서버에서 다음과 같이 /uploads 폴더에 이미지를 저장한다고 하자.
// upload.controller.ts
import express from 'express';
import path from 'path';
import multer from 'multer';
const app = express();
const upload = multer({ dest: 'uploads/' });
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.post('/api/upload', upload.single('file'), (req, res) => {
res.json({ url: `/uploads/${req.file.filename}` });
});
서버를 실행하고 이미지를 업로드하면 /uploads/파일명 형태의 URL이 생성된다.
하지만 업로드 직후 브라우저에서 해당 경로를 요청하면 404가 발생하는 경우가 있다.
그 이유는,
Express의 정적 파일 미들웨어(express.static)는 서버 실행 시점에 이미 존재하는 파일만 인식한다.
즉, 서버 재시작 전까지는 새로 업로드된 파일을 정적 경로로 접근할 수 없다.
서버가 실행된 뒤 새 파일이 추가되면,
정적 라우팅 테이블에는 반영되지 않으므로
브라우저가 /uploads/...로 요청해도 “없는 파일”로 간주되어 404를 반환한다.
이 문제를 해결하려면,
서버가 파일 시스템에서 직접 파일을 읽어서 응답으로 전송해야 한다.
즉, 파일 스트리밍 방식으로 파일을 제공해야 한다.
2. 클라이언트가 이미지를 못 찾는 경우 (경로 문제)
서버는 정상적으로 파일을 저장했지만, 클라이언트에서 요청 경로를 잘못 지정한 경우다.
예를 들어 서버에는 /uploads/abc.jpg가 존재하지만
프론트엔드가 /public/uploads/abc.jpg로 요청한다면 404가 발생한다.
또한 서버에서 /uploads 경로를 express.static()으로 노출하지 않았다면
파일이 실제로 존재하더라도 브라우저는 접근할 수 없다.
3. Next.js에서의 파일 스트리밍 API
Next.js에서는 public/ 폴더가 정적으로 노출되지만,
이는 빌드 시점에 존재하던 파일만 포함한다.
즉, 서버가 실행된 이후 업로드된 파일은
서버를 재시작하기 전까지 정적 경로로 접근할 수 없다.
이런 파일을 즉시 표시하려면, 서버가 직접 읽어서 전송하는
파일 스트리밍 API를 사용해야 한다.
// pages/api/file.ts
import type { NextApiRequest, NextApiResponse } from "next";
import fs from "fs";
import path from "path";
const mimeTypes: Record<string, string> = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".mp4": "video/mp4",
".mp3": "audio/mpeg",
};
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { name, content_id } = req.query;
if (!name || !content_id) {
res.status(400).send("Missing parameters");
return;
}
const filePath = path.join(process.cwd(), "public", "uploads", String(content_id), String(name));
const ext = path.extname(filePath).toLowerCase();
const mimeType = mimeTypes[ext] || "application/octet-stream";
if (!fs.existsSync(filePath)) {
res.status(404).send("File not found");
return;
}
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = end - start + 1;
const fileStream = fs.createReadStream(filePath, { start, end });
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunkSize,
"Content-Type": mimeType,
"Content-Disposition": `attachment; filename="${name}"`,
});
fileStream.pipe(res);
} else {
res.writeHead(200, {
"Content-Length": fileSize,
"Content-Type": mimeType,
});
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
}
}
왜 “파일 스트리밍”이 필요한가?
- 서버 실행 중 추가된 파일 접근 문제 해결
Next.js나 Express의 정적 라우팅은 서버 시작 시 결정된다.
따라서, 새로 업로드된 파일은 정적 경로로 접근할 수 없다.
→ 스트리밍 API는 파일 시스템에서 직접 읽기 때문에, 서버 재시작 없이도 접근 가능하다. - 대용량 파일 처리에 효율적
fs.createReadStream()은 파일을 메모리에 모두 올리지 않고 조각(chunk) 단위로 전송한다.
메모리 사용량을 최소화하고, 영상·음원 같은 큰 파일도 안정적으로 제공할 수 있다. - Range 요청(부분 재생) 지원
브라우저가 "Range" 헤더를 보낼 경우, 해당 구간만 스트리밍 가능하다.
영상 시킹(seeking) 시 즉시 반응할 수 있다.
MIME 타입 지정
const mimeTypes: Record<string, string> = { ".png": "image/png", ".mp4": "video/mp4" };
응답의 Content-Type이 올바르지 않으면 브라우저는 이미지를 표시하지 못하고 다운로드해버린다.
따라서 스트리밍 시 MIME 타입은 필수적으로 설정해야 한다.
보안 측면
파일 스트리밍 API는 단순히 파일을 읽는 기능을 넘어서,
보안적으로 제어 가능한 파일 접근 방식을 제공한다.
- public 폴더를 직접 노출하지 않음
- 사용자 인증/권한 검사 로직 추가 가능
- 경로 탐색 공격(path traversal) 방지 (../../../etc/passwd 차단)
결론
서버가 실행된 이후 업로드된 파일은
정적 경로(static route) 로는 읽히지 않는다.
서버를 재시작하지 않는 이상,
파일 스트리밍 API를 통해서만 접근이 가능하다.
이 방식은 단순히 이미지를 보여주는 기능이 아니라,
서버가 실시간으로 파일을 읽고 전송할 수 있게 하는 핵심 구조이다!
'Framework > Next.js' 카테고리의 다른 글
| [Next.js] tailwind + clsx 라이브러리 (0) | 2025.10.24 |
|---|---|
| [Next.js] Link 와 router 차이점 (0) | 2025.10.21 |
| [Next.js] Prisma + PostgreSQL (0) | 2025.10.05 |
| [Next.js] 윈도우 배치파일 설정 (0) | 2025.10.05 |
| [Next.js] 전체화면 F12 설정 (0) | 2025.10.05 |