프론트엔드만 해오다가 백엔드 원리를 이해하고 간단한 CRUD 웹 페이지를 만들어보고 싶어서 Node.js로 짧게 실습해봤다. 간단한 회원가입/로그인이 있는 게시판 페이지를 개발했다.
백엔드 이해가 주 목적이었기 때문에 프론트는 간단하게 형태만 구현하였다.
JavaScript를 브라우저 밖, 즉 서버에서 실행할 수 있게 해주는 런타임. Next.js가 돌아가는 환경도 Node.js다.
Node.js용 웹 프레임워크. 라우팅, 미들웨어, 요청/응답 처리를 간단하게 해준다.
const express = require("express");
const app = express();
app.listen(3000, () => console.log("listening on 3000"));
파일이 바뀔 때 서버를 자동으로 재시작해주는 도구. 개발할 때 필수.
nodemon server.js
| 메서드 | 용도 | 예시 |
|---|---|---|
| GET | 조회 | 글 목록, 상세 |
| POST | 생성 | 글 작성, 로그인 |
| PUT | 수정 | 글 수정 |
| DELETE | 삭제 | 글 삭제 |
app.get("/posts/:id", (req, res) => {
const id = req.params.id;
res.json({ id });
});
:id → req.params.id?page=1 → req.query.pagereq.body (express.json() 미들웨어 필요)요청과 응답 사이에 끼어드는 함수. next()로 다음으로 넘긴다.
app.use(express.json()); // body JSON 파싱
app.use(express.urlencoded({ extended: true }));
app.use(express.static("public")); // 정적 파일 제공
로그인 여부를 확인하는 인증 미들웨어 예시:
function requireLogin(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: "로그인 필요" });
}
next();
}
// 사용
app.post("/posts", requireLogin, (req, res) => { ... });
better-sqlite3는 동기식이라 async/await 없이 직관적으로 쓸 수 있다.
const Database = require("better-sqlite3");
const db = new Database("posts.db");
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
| 메서드 | 용도 |
|---|---|
prepare(SQL).run(...) | INSERT / UPDATE / DELETE |
prepare(SQL).get(...) | SELECT 1개 행 |
prepare(SQL).all(...) | SELECT 여러 행 |
? 플레이스홀더값을 SQL 문자열에 직접 박지 않고 ?로 전달한다. SQL 인젝션 방어가 핵심.
// ❌ 위험
db.prepare(`SELECT * FROM users WHERE id = ${id}`);
// ✅ 안전
db.prepare("SELECT * FROM users WHERE id = ?").get(id);
// 작성
const result = db
.prepare("INSERT INTO posts (title, content) VALUES (?, ?)")
.run(title, content);
// 작성된 행 다시 조회 (id, createdAt 등 자동 생성값 받으려고)
const newPost = db
.prepare("SELECT * FROM posts WHERE id = ?")
.get(result.lastInsertRowid);
// 목록
const posts = db.prepare("SELECT * FROM posts ORDER BY createdAt DESC").all();
// 수정
db.prepare("UPDATE posts SET title = ?, content = ? WHERE id = ?")
.run(title, content, id);
// 삭제
db.prepare("DELETE FROM posts WHERE id = ?").run(id);
게시글에 댓글을 다는 1:N 관계를 만들 때 외래키(FOREIGN KEY) 를 쓴다.
CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postId INTEGER NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (postId) REFERENCES posts(id) ON DELETE CASCADE
);
ON DELETE CASCADE: 부모(글)가 삭제되면 자식(댓글)도 자동 삭제db.pragma("foreign_keys = ON");
댓글 조회 시 작성자 이름까지 같이 가져오기:
SELECT comments.*, users.username
FROM comments
JOIN users ON comments.userId = users.id
WHERE postId = ?
ORDER BY comments.createdAt
비밀번호를 평문으로 DB에 저장하면 DB가 털릴 때 그대로 노출된다. 단방향 해싱으로 저장하고, 로그인 시 비교만 한다.
// 회원가입
const hashed = await bcrypt.hash(password, 10);
// 로그인
const ok = await bcrypt.compare(password, user.password);
10은 cost factor. 클수록 해싱 느려져서 무차별 대입 방어에 유리.HTTP는 stateless라 요청마다 누가 보냈는지 서버가 잊는다. 세션은 서버가 "누구인지" 기억하는 방식.
app.use(session({
secret: "비밀키",
resave: false,
saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 * 24 }, // 1일
}));
흐름:
req.session.destroy()app.post("/auth/login", async (req, res) => {
const { username, password } = req.body;
const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username);
if (!user) return res.status(401).json({ error: "아이디/비밀번호 틀림" });
const ok = await bcrypt.compare(password, user.password);
if (!ok) return res.status(401).json({ error: "아이디/비밀번호 틀림" });
req.session.userId = user.id;
req.session.username = user.username;
res.json({ id: user.id, username: user.username });
});
requireLogin 미들웨어app.put("/posts/:id", requireLogin, (req, res) => {
const post = db.prepare("SELECT * FROM posts WHERE id = ?").get(postId);
if (post.userId !== req.session.userId) {
return res.status(403).json({ error: "권한 없음" });
}
// ...수정 로직
});
같은 도메인(localhost:3000)에서 서비스하면 CORS 신경 안 써도 되고, 세션 쿠키도 자동 첨부된다.
app.use(express.static("public"));
const res = await fetch("/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});
const data = await res.json();