첫 번째 Express 서버를 만들면서 배운 점

발행: (2026년 5월 24일 AM 05:21 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

저는 주로 프론트엔드 개발자를 합니다. React, Vite, Tailwind 등—그게 제 세계죠. 백엔드는 언제나 존재한다는 건 알지만 거의 손대보지 않았던 영역이었습니다. 오늘은 드디어 자리를 잡고 처음으로 Express 서버를 처음부터 만들었고, 실제로 이해가 된 부분과 처음엔 안됐던 부분을 모두 기록하고 싶습니다.
이 글은 깔끔한 튜토리얼이라기보다는 제가 배운 점, 헷갈렸던 점, 그리고 다시 시작한다면 스스로에게 해줄 말을 솔직히 적은 기록입니다.

Setting up from zero

먼저 해야 할 일:

# 터미널에 아래 명령어를 실행하세요
npm init -y
npm install express

그 다음 server.js 파일을 만들고 최소한의 서버 코드를 작성했습니다:

const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

node server.js 로 실행하고 브라우저에서 http://localhost:3000 을 열면 “Hello world” 가 보입니다. 약 10줄 정도면 동작하는 HTTP 서버가 완성된 것이죠.

각 부분이 실제로 무슨 일을 하는지 하나씩 살펴보겠습니다.


Breaking down the code

const express = require('express')

node_modules 안에 있는 Express 라이브러리를 현재 파일로 가져옵니다. require는 Node의 기본 모듈 시스템인 CommonJS입니다. 가져온 값을 express 라는 변수에 저장합니다.

const app = express()

express() 를 함수처럼 호출합니다. 이 함수는 애플리케이션 객체를 반환하는데, 바로 이 app 이 서버의 제어판 역할을 합니다. 모든 설정과 라우트는 여기서 연결됩니다.

라우트

app.get('/', (req, res) => {
  res.send('Hello world');
});

라우트를 등록합니다. 크게 읽어보면:

“누군가 / 로 GET 요청을 하면, 이 함수를 실행한다.”

  • req : 요청 객체 — 클라이언트가 보낸 모든 정보(URL, 쿼리 파라미터, 헤더, 바디 등)
  • res : 응답 객체 — 클라이언트에게 무언가를 돌려줄 때 사용하는 도구
app.listen(3000, () => { ... })

포트 3000 번에서 서버를 시작합니다. 콜백은 서버가 준비된 뒤에 실행되므로, 로그를 여기서 찍는 것이 맞습니다.

처음엔 reqres 가 Express가 강제하는 특별 키워드라고 착각했는데, 사실은 단순히 파라미터 이름일 뿐입니다. Express는 언제나 요청 객체를 첫 번째, 응답 객체를 두 번째 인자로 콜백에 전달합니다. 이름은 자유롭게 정할 수 있습니다:

// 모두 동일하게 동작합니다
app.get('/', (req, res) => { res.send('ok') });
app.get('/', (request, response) => { response.send('ok') });
app.get('/', (banana, mango) => { mango.send('ok') });

마지막 예시도 완전한 JavaScript이며, mango 가 응답 객체이기 때문에 .send() 를 정상적으로 사용할 수 있습니다.
커뮤니티에서는 reqres 가 관례이므로, 코드를 읽는 사람들이 바로 어느 것이 무엇인지 알 수 있습니다. 관례를 따르는 것이 좋지만, 이것이 단지 관례라는 걸 알면 전체가 덜 신비롭게 느껴집니다.

GET 요청에서 클라이언트가 데이터를 보내는 두 가지 방법

app.get('/user/:id', (req, res) => {

: 앞에 붙은 id 는 Express에게 해당 URL 세그먼트가 변수임을 알려줍니다. 따라서 /user/jeffrey, /user/42, /user/anything 모두 이 라우트와 매치됩니다. 변수에 들어간 값은 req.params.id 에 저장됩니다.
접근 예시: http://localhost:3000/user/jeffrey

여러 파라미터를 사용할 때는 쉼표가 아니라 슬래시 로 구분합니다:

app.get('/add/:a/:b', (req, res) => { ... })
app.get('/add/:a,:b', (req, res) => { ... }) // ❌ 잘못된 예시

쉼표를 사용한 실수 때문에 시간을 낭비한 적이 있습니다.

쿼리 스트링을 사용할 경우는 이렇게 합니다:

app.get('/add', (req, res) => { ... })

접근 예시: http://localhost:3000/add?a=10&b=5

언제 어떤 방식을 써야 할까?

  • URL 파라미터 : 특정 리소스를 식별할 때 사용합니다. 예) /user/42, /product/air-max
  • 쿼리 파라미터 : 연산, 필터, 선택적 값 등에 더 적합합니다. 예) /search?q=express, /add?a=10&b=5

수학 연산 같은 경우는 쿼리 파라미터가 자연스럽습니다.

로직을 모듈로 분리하기

서버 파일 안에 모든 함수를 넣는 대신, 별도의 math.js 파일을 만들었습니다:

// math.js
function add(a, b) {
  return a + b;
}
function mult(a, b) {
  return a * b;
}
module.exports = { add, mult };

그리고 서버 파일에서 이를 가져옵니다:

const { add, mult } = require('./math');

이렇게 하면 서버 파일이 깔끔해집니다. 라우트는 HTTP를 담당하고, 모듈은 비즈니스 로직을 담당하니까요. 초기에 이런 습관을 들이면 좋습니다.


CommonJS vs ESModules

Node에는 두 가지 모듈 시스템이 존재하며, 오늘은 그 사이를 왔다 갔다 했습니다.

CommonJS

const express = require('express');
module.exports = { add, mult };

ESModules

import express from 'express';
export { add, mult };

핵심 차이점은 다음과 같습니다:

  • Node에서 ESM을 사용하려면 package.json"type": "module" 을 명시해야 합니다. 그렇지 않으면 .js 파일은 기본적으로 CJS로 취급됩니다.
  • ESM에서는 로컬 파일을 import 할 때 파일 확장자를 반드시 써야 합니다: ./math.js ( ./math 은 안 됨)
  • 두 시스템을 자유롭게 섞을 수 없습니다. ESM 파일 안에 require 를 쓰면 오류가 나고, CJS 파일 안에 import 를 쓰면 오류가 납니다.

초보자에게 권장하는 방법

특별한 이유가 없다면 CommonJS 를 고수하세요. 설정이 필요 없고 바로 사용할 수 있으며, 대부분의 Express 튜토리얼과 패키지가 이 방식을 사용합니다. 구체적인 필요가 생겼을 때 ESM으로 전환하면 됩니다.


POST 요청 다루기

GET 요청은 데이터를 URL에 담아 보냅니다. POST 요청은 바디에 데이터를 담아 보냅니다—예를 들어 로그인 폼을 제출하거나 JSON 형태의 API를 호출할 때 사용합니다. 비밀번호 같은 민감한 정보가 브라우저 히스토리에 남는 것을 방지할 수 있죠.

app.post('/login', (req, res) => {
  const { username } = req.body;
  res.send(`Welcome ${username}`);
});

하지만 여기엔 함정이 있습니다. req.body 를 읽으려면 라우트 선언 에 다음 코드를 반드시 넣어야 합니다:

app.use(express.json());

app.use() 는 들어오는 모든 요청에 미들웨어를 적용합니다. express.json() 은 원시 요청 바디를 읽어 JSON 으로 파싱해 주어 req.body 로 접근할 수 있게 해 줍니다.

이 라인을 빼고 테스트해 보면 다음과 같은 결과가 나옵니다:

undefined

즉, req.bodyundefined 로 남습니다. 미들웨어는 선택 사항이 아니라 필수이며, 라우트보다 먼저 선언해야 합니다.

POST 요청을 테스트하려면 브라우저 주소창이 아니라 Thunder Client(VS Code 확장) 혹은 Postman 을 사용하세요. 예시:

  • URL: http://localhost:3000/login
  • Method: POST
  • Body (JSON):
    {
      "username": "jeffrey"
    }

자주 놓치는 포인트

  1. app.listen 콜백

    // 잘못된 예시 — 서버가 준비되기 전에 로그가 출력됨
    app.listen(3000, console.log("Server running on port 3000"));
    
    // 올바른 예시 — 서버가 실제로 올라간 뒤에 로그가 출력됨
    app.listen(3000, () => console.log("Server running on port 3000"));

    화살표 함수를 쓰지 않으면 파일이 로드될 때 바로 console.log 가 실행됩니다. 미묘하지만 중요한 차이죠.

  2. res.send() vs res.json()
    res.send() 은 일반 텍스트용, res.json() 은 객체와 구조화된 데이터를 반환할 때 사용합니다. API를 만들 때는 거의 항상 res.json() 을 쓰는 것이 좋습니다.

  3. **서버 재시작을 잊

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.