Node.js 백엔드 입문 - Express + SQLite로 게시판 만들기

LeeKyungwon·2026년 6월 24일

공부 정리

목록 보기
34/34

프론트엔드만 해오다가 백엔드 원리를 이해하고 간단한 CRUD 웹 페이지를 만들어보고 싶어서 Node.js로 짧게 실습해봤다. 간단한 회원가입/로그인이 있는 게시판 페이지를 개발했다.
백엔드 이해가 주 목적이었기 때문에 프론트는 간단하게 형태만 구현하였다.

학습 환경

  • Node.js v20
  • Express
  • better-sqlite3 (SQLite)
  • bcrypt, express-session
  • 프론트는 바닐라 HTML/CSS/JS

1. Node.js & Express 기본

Node.js란?

JavaScript를 브라우저 밖, 즉 서버에서 실행할 수 있게 해주는 런타임. Next.js가 돌아가는 환경도 Node.js다.

Express

Node.js용 웹 프레임워크. 라우팅, 미들웨어, 요청/응답 처리를 간단하게 해준다.

const express = require("express");
const app = express();

app.listen(3000, () => console.log("listening on 3000"));

nodemon

파일이 바뀔 때 서버를 자동으로 재시작해주는 도구. 개발할 때 필수.

nodemon server.js

2. HTTP 메서드와 REST API

메서드용도예시
GET조회글 목록, 상세
POST생성글 작성, 로그인
PUT수정글 수정
DELETE삭제글 삭제

라우터 작성

app.get("/posts/:id", (req, res) => {
  const id = req.params.id;
  res.json({ id });
});
  • :idreq.params.id
  • ?page=1req.query.page
  • POST/PUT body → req.body (express.json() 미들웨어 필요)

HTTP 상태 코드

  • 200 OK
  • 201 Created
  • 204 No Content (DELETE 성공 시)
  • 400 Bad Request
  • 401 Unauthorized (로그인 안 됨)
  • 403 Forbidden (권한 없음)
  • 404 Not Found
  • 500 Server Error

3. 미들웨어

요청과 응답 사이에 끼어드는 함수. 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) => { ... });

4. SQLite로 DB 연결하기

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);

CRUD 예시

// 작성
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);

5. 1:N 관계 (외래키)

게시글에 댓글을 다는 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: 부모(글)가 삭제되면 자식(댓글)도 자동 삭제
  • SQLite는 외래키가 기본 OFF라 활성화 필요:
db.pragma("foreign_keys = ON");

JOIN

댓글 조회 시 작성자 이름까지 같이 가져오기:

SELECT comments.*, users.username
FROM comments
JOIN users ON comments.userId = users.id
WHERE postId = ?
ORDER BY comments.createdAt

6. 인증: 회원가입 & 로그인

bcrypt로 비밀번호 해싱

비밀번호를 평문으로 DB에 저장하면 DB가 털릴 때 그대로 노출된다. 단방향 해싱으로 저장하고, 로그인 시 비교만 한다.

// 회원가입
const hashed = await bcrypt.hash(password, 10);

// 로그인
const ok = await bcrypt.compare(password, user.password);
  • 10은 cost factor. 클수록 해싱 느려져서 무차별 대입 방어에 유리.
  • 같은 비밀번호라도 매번 다른 해시가 나오지만 비교는 정상 동작.

세션 (express-session)

HTTP는 stateless라 요청마다 누가 보냈는지 서버가 잊는다. 세션은 서버가 "누구인지" 기억하는 방식.

app.use(session({
  secret: "비밀키",
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 1000 * 60 * 60 * 24 }, // 1일
}));

흐름:

  1. 로그인 성공 → 서버가 세션 생성, 세션 ID를 쿠키로 발급
  2. 브라우저는 쿠키를 자동 저장
  3. 다음 요청부터 쿠키 자동 첨부 → 서버가 세션 ID로 누군지 식별
  4. 로그아웃 → 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 });
});

인증 vs 인가

  • 인증(Authentication): 로그인 됐는가? → requireLogin 미들웨어
  • 인가(Authorization): 이 작업을 할 권한이 있는가? → 본인 글만 수정 가능
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: "권한 없음" });
  }
  // ...수정 로직
});

7. 프론트-백엔드 연동

같은 도메인(localhost:3000)에서 서비스하면 CORS 신경 안 써도 되고, 세션 쿠키도 자동 첨부된다.

app.use(express.static("public"));

fetch로 API 호출

const res = await fetch("/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title, content }),
});
const data = await res.json();

완성한 기능

  • ✅ 게시판 CRUD
  • ✅ 댓글 CRUD (1:N 관계 + CASCADE 삭제)
  • ✅ 회원가입 / 로그인 / 로그아웃
  • ✅ 세션 기반 인증
  • ✅ 본인 글/댓글만 수정·삭제 가능 (인가)
  • ✅ JOIN으로 작성자 정보 함께 조회

0개의 댓글