Node.js는 브라우저 내부에 갇혀 있던 JavaScript를 서버사이드(Server-side) 환경으로 끌어낸 혁신적인 런타임입니다. 구글의 초고속 V8 엔진과 비동기 처리를 위한 이벤트 기반의 논블로킹(Non-blocking) I/O 아키텍처를 결합하여, 가볍고 효율적이면서도 압도적인 성능을 내는 네트워크 애플리케이션을 구축할 수 있습니다.
Node.js의 심장은 구글 크롬 브라우저를 위해 개발된 V8 JavaScript 엔진입니다. C++로 작성된 이 엔진은 코드를 실행할 때마다 한 줄씩 해석하는 인터프리터(Interpreter) 방식의 한계를 극복하기 위해, 실행 직전에 소스 코드를 기계어(Machine Code)로 즉시 번역해버리는 JIT(Just-In-Time) 컴파일러를 탑재하여 C, C++에 버금가는 놀라운 실행 속도를 제공합니다.
기존의 서버 언어(PHP, Java 등)는 DB를 조회하거나 파일을 읽을 때 응답이 올 때까지 스레드(Thread)가 멈춰서 기다리는 블로킹(Blocking) 모델을 사용합니다. 하지만 Node.js는 이런 무거운 입출력(I/O) 작업을 던져놓고 멈춤 없이 즉시 다음 코드를 실행하러 가는 논블로킹(Non-blocking) 모델을 채택하여 서버 자원을 극도로 효율적으로 사용합니다.
| 구분 | 코드 및 서버 동작 방식 | 실생활 비유 (카페 주문) |
|---|---|---|
| 블로킹 (Blocking) | 1번 작업이 끝날 때까지 CPU가 아무 일도 못하고 2번 작업을 대기시킴. (동기) | 직원이 앞사람의 커피를 다 만들어 건네줄 때까지, 뒷사람이 주문도 못하고 줄 서서 멍하니 대기함. |
| 논블로킹 (Non-blocking) | 1번 작업을 백그라운드에 던지고 즉시 2번 작업 실행. 완료되면 콜백으로 알림. (비동기) | 직원이 주문만 연속으로 받고 손님에게 진동벨을 줌. 커피가 나오는 대로(작업 완료) 진동벨을 울림. |
"자바스크립트는 싱글 스레드인데 어떻게 동시에 수많은 비동기 작업을 처리하나요?" 라는 질문에 대한 해답이 바로 이벤트 루프(Event Loop)와 libuv 라이브러리입니다. 무거운 작업은 백그라운드의 워커 스레드 풀(Thread Pool)이나 OS 커널로 넘기고, 메인 스레드(이벤트 루프)는 오직 작업 완료 알림(콜백)만 부지런히 큐(Queue)에서 꺼내어 실행하기 때문에 단 1개의 스레드만으로 수만 개의 연결을 처리할 수 있습니다.
위의 터미널 목업 로그를 보면 Node.js의 비동기적 특성을 명확히 알 수 있습니다. 코드의 작성 순서는 1 → 3 → 2 번이지만, 실제 콘솔 출력 결과는 1 → 2 → 3 번 순서로 찍히는 것을 확인할 수 있습니다. 무거운 I/O 작업(파일 읽기)을 백그라운드로 넘겨둔 채 메인 스레드는 즉시 2번 로직을 처리하고, 추후 파일 읽기가 완료되면 이벤트 루프가 3번 콜백을 호출해 주는 완벽한 논블로킹(Non-blocking) 흐름을 보여줍니다.
Node.js가 싱글 스레드임에도 불구하고 수천 개의 동시 접속을 효율적으로 처리할 수 있는 비밀은 바로 이벤트 루프에 있습니다. 이벤트 루프는 메인 스레드(Call Stack)가 비어있는지를 지속적으로 감시하고, 백그라운드 작업이 완료되어 큐(Queue)에 등록된 콜백 함수들을 순차적으로 실행 공간(Call Stack)으로 밀어 올리는 심장과 같은 역할을 수행합니다.
Node.js의 큐는 단일한 큐가 아니라 실행 타이밍과 우선순위에 따라 여러 종류로 나뉩니다. 가장 대표적인 두 가지를 비교해 보겠습니다.
| 분류 | 해당하는 함수들 | 실행 우선순위 | 특징 |
|---|---|---|---|
| 마이크로태스크 큐 (Microtask Queue) |
Promise.then(), process.nextTick(), queueMicrotask() |
매우 높음 (1순위) | 현재 Call Stack이 비워지자마자 가장 먼저 실행됩니다. |
| 매크로태스크 큐 (Task Queue) |
setTimeout(), setInterval(), setImmediate() |
보통 (마이크로태스크 이후) | 마이크로태스크 큐가 완전히 비워진 후(고갈 상태)에만 이벤트 루프가 접근하여 하나씩 꺼내 실행합니다. |
아래 코드는 동기 작업, 마이크로태스크(Promise), 매크로태스크(setTimeout)가 섞여 있을 때의 극단적인 시나리오입니다. 작성된 코드의 순서와 상관없이, Node.js의 이벤트 루프 우선순위에 의해 실행 결과가 완전히 뒤바뀌는 것을 콘솔 출력에서 확인할 수 있습니다.
과거 Node.js는 브라우저와 독립적으로 자바스크립트 파일을 모듈로 나누기 위해 독자적인 CommonJS (CJS) 규격을 사용했습니다. 하지만 최근 자바스크립트 표준 위원회에서 ES Modules (ESM)라는 공식 표준을 제정함에 따라, 현재 Node.js는 두 가지 모듈 시스템을 모두 지원하는 과도기를 겪고 있습니다. 이 두 시스템이 런타임에서 엔진에 의해 어떻게 다르게 해석되고 로드되는지 이해하는 것은 매우 중요합니다.
Node.js는 기본적으로 확장자가 .js인 파일을 CommonJS로 취급합니다. Node.js 환경에서 ES Modules를 사용하려면 package.json 파일에 "type": "module" 필드를 추가하거나, 파일 확장자를 .mjs로 명시적으로 변경해야 합니다.
| 특징 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 불러오기 문법 | const lib = require('module') |
import lib from 'module' |
| 내보내기 문법 | module.exports = { ... } |
export const lib = ... |
| 분석 시점 | 런타임(Runtime) 동적 분석 (코드 실행 중 동적으로 require 발생 가능) |
빌드/파싱 타임 정적 분석 (실행 전 최상단에서 의존성 맵 완벽 구성) |
| Tree-Shaking 지원 | 어려움 (불필요한 코드도 함께 번들링) | 강력하게 지원 (사용하지 않는 코드 제거) |
| Top-level Await | 지원하지 않음 | 지원함 (함수 밖에서 await 사용 가능) |
| 기본 파일 확장자 | .cjs (또는 type 명시 안된 .js) |
.mjs (또는 type:"module" 인 .js) |
CommonJS는 require() 함수를 만나는 순간 동기적으로 실행을 멈추고 파일을 읽어와서 해석(평가)합니다.
ESM은 실제 코드를 실행하기 전에 전체 파일의 import 구조를 먼저 정적(Static)으로 파악하여 연결(Linking)한 뒤 비동기적으로 실행합니다. 이 덕분에 불필요한 코드를 제거하는 Tree-Shaking이 가능합니다.
NPM은 전 세계 자바스크립트 개발자들이 만들어 놓은 수많은 코드(패키지)를 손쉽게 다운로드하고 관리할 수 있게 해주는 도구입니다. Node.js 프로젝트의 심장과도 같은 package.json 파일은 프로젝트의 메타데이터와 더불어, 어떤 패키지의 몇 버전을 사용하는지(Dependencies)를 기록하는 중요한 명세서 역할을 합니다.
패키지 설치 시 자동으로 생성되는 파일로, 하위 의존성(dependency의 dependency)까지 포함하여 설치된 모든 패키지의 정확한 버전 트리를 고정(Lock)합니다. 이를 통해 다른 개발자나 배포 서버에서도 완전히 동일한 버전의 패키지들이 설치되는 것을 보장합니다.
| 버전 표기법 (기호) | 의미 및 동작 방식 (Semantic Versioning) |
|---|---|
| "express": "4.18.2" | 정확한 버전 (Exact Version): 기호가 없으면 오직 4.18.2 버전만 설치합니다. |
| "express": "^4.18.2" | Caret (캐럿) - 권장: 마이너(Minor) 및 패치(Patch) 버전을 자동 업데이트합니다. (ex. 4.19.0, 4.18.5 허용 / 5.0.0 불가) |
| "express": "~4.18.2" | Tilde (틸드): 오직 패치(Patch) 버전만 자동 업데이트합니다. (ex. 4.18.9 허용 / 4.19.0 불가) |
| 패키지 옵션 | 명령어 예시 | 사용 목적 |
|---|---|---|
| dependencies | npm install [패키지명] |
실제 앱 실행(프로덕션)에 반드시 필요한 핵심 라이브러리 (ex: express, react) |
| devDependencies | npm install -D [패키지명] |
개발 단계에서만 사용되고 실제 서비스 배포 시에는 제외되는 도구들 (ex: nodemon, jest, typescript) |
아래 터미널 로그는 빈 프로젝트에서 npm init -y로 초기화한 뒤, npm install express 명령어로 외부 패키지를 설치했을 때 파일 시스템과 터미널에서 일어나는 변화를 보여줍니다.
브라우저 환경에 전역 객체 window가 있다면, Node.js 환경에는 global 객체가 존재합니다. Node.js는 스크립트가 실행될 때마다 파일 경로, 운영체제 정보, 환경 변수 등 개발에 필수적인 정보들을 process 객체나 전역 변수 형태로 기본 제공합니다. 특히 파일 경로를 다룰 때는 __dirname과 process.cwd()의 미묘한 차이를 정확히 구분해야 경로 에러를 피할 수 있습니다.
package.json에 "type": "module"을 설정한 ES Modules (ESM) 환경에서는 __dirname과 __filename 전역 변수를 사용할 수 없습니다. ESM에서는 대신 import.meta.url 객체를 파싱하여 파일 및 디렉토리 경로를 직접 구해야 합니다.
| 이름 | 역할 및 설명 |
|---|---|
| process | 현재 실행 중인 Node.js 프로세스에 대한 정보와 제어를 제공하는 전역 객체입니다. ex: process.env(환경변수), process.exit()(종료) |
| __dirname | 현재 실행 중인 스크립트 파일이 위치한 디렉토리의 절대 경로 문자열입니다. |
| __filename | 현재 실행 중인 스크립트 파일의 이름까지 포함된 절대 경로 문자열입니다. |
| console | 브라우저의 콘솔과 유사하게 stdout/stderr 표준 출력 스트림으로 로그를 출력하는 내장 객체입니다. |
| global | Node.js의 최상위 전역(Global) 네임스페이스 객체입니다. (브라우저의 window 객체와 동일한 위상) |
루트 폴더(/projects/my-app)에서 src/utils/logger.js 파일을 실행했을 때, 각 전역 변수들이 정확히 어떤 경로를 반환하는지 터미널 출력 결과로 확인해 보세요.
브라우저 환경의 자바스크립트는 보안상의 이유로 사용자의 로컬 컴퓨터 파일을 마음대로 읽거나 쓸 수 없습니다. 하지만 Node.js는 서버 환경에서 동작하기 때문에 운영체제의 파일 시스템(File System)에 직접 접근할 수 있는 권한을 가집니다. 이를 가능하게 해주는 핵심 내장 모듈이 바로 fs (File System) 모듈입니다.
Node.js는 싱글 스레드입니다. 만약 동기식(Sync) 메서드를 사용해 1GB짜리 거대한 파일을 읽는다면, 그 파일을 전부 읽어올 때까지 서버가 멈춰버립니다. 그동안 다른 사용자들이 요청한 접속은 전혀 처리되지 못하고 무한 대기 상태에 빠집니다. 따라서 Node.js에서는 특별한 초기화 스크립트 단계를 제외하고는 무조건 비동기 메서드를 사용하는 것이 원칙입니다.
| 모듈 임포트 방식 | 함수 예시 | 특징 및 권장 여부 |
|---|---|---|
const fs = require('fs') |
fs.readFile(path, cb) | 전통적인 콜백(Callback) 방식입니다. 여러 작업을 연속으로 처리할 때 '콜백 지옥(Callback Hell)'에 빠질 위험이 있습니다. |
const fs = require('fs').promises |
await fs.readFile(path) | 모던 자바스크립트 권장 방식! 비동기를 처리할 때 Promise 반환을 지원하므로 async/await 문법을 사용해 코드를 동기식처럼 깔끔하게 작성할 수 있습니다. |
실제 코드로 비동기 파일 시스템 접근을 테스트해 보면, 거대한 파일을 읽는 동안에도 메인 스레드가 멈추지 않고 다음 명령어를 즉시 처리하는 것을 콘솔 로그 순서를 통해 증명할 수 있습니다.
개발 환경(Windows)과 배포 환경(Linux, macOS)은 폴더 경로 구분자(\ vs /)나 CPU 아키텍처가 서로 다릅니다. Node.js는 이러한 운영체제 간의 차이를 완벽히 추상화하여 동일한 코드로 작동할 수 있게 해주는 path 모듈과 os 모듈을 기본으로 제공합니다.
| 함수명 | 동작 원리 | 예시 (Windows 환경 기준) |
|---|---|---|
| path.join() | 주어진 인자들을 하나로 단순히 합치며(결합), ..(상위 폴더)나 .(현재 폴더)를 알아서 계산해 줍니다. 절대 경로 /가 중간에 있어도 상대 경로처럼 무시하고 단순 합체합니다. |
path.join('a', 'b', '/c') // 결과: a\b\c |
| path.resolve() | 오른쪽에서 왼쪽으로 읽으며, 가장 먼저 만나는 루트 절대경로(/)를 기준으로 최종 경로를 반환합니다. 절대경로 앞의 값들은 완전히 무시됩니다! |
path.resolve('a', 'b', '/c') // 결과: C:\c |
서버의 자원 상태(CPU, 메모리 부족 등)를 모니터링할 때 주로 쓰입니다.
실제 스크립트 파일에서 path.join()과 path.resolve()를 실행했을 때 반환되는 경로 문자열의 차이점, 그리고 os 모듈이 뱉어내는 운영체제 시스템 정보를 터미널에서 확인해 보세요.
브라우저 자바스크립트에서 onClick 같은 이벤트를 다루듯, Node.js 서버 내부에서도 "파일 읽기가 끝났어!", "DB 접속이 끊어졌어!" 같은 상황을 이벤트(Event) 기반으로 소통합니다. 이를 총괄하는 가장 중요한 내장 클래스가 바로 EventEmitter입니다.
EventEmitter에서 'error'라는 이름의 이벤트는 특별하게 취급됩니다. 에러 이벤트가 발생(emit)되었는데, 이를 처리할 리스너(.on('error', ...))가 하나도 없다면 Node.js 프로그램 자체가 강제로 멈추고 종료(Crash)되어 버립니다.
이벤트를 수신 대기(Listen)하고, 특정 타이밍에 이벤트를 방출(Emit)하는 과정을 콘솔에서 확인해 보세요.
2GB짜리 영화 파일을 메모리 1GB 서버에서 전송하려면 어떻게 해야 할까요? 전체를 한 번에 올리면 메모리 초과(OOM)로 서버가 죽습니다. 이럴 때 데이터를 작게 잘라(Buffer) 흐르게(Stream) 만드는 기술이 필수적입니다.
버퍼는 0과 1로 이루어진 바이너리(이진) 데이터를 임시로 담아두는 고정 크기의 메모리 덩어리입니다. V8 엔진(자바스크립트)이 아닌 C++ 레벨에서 할당된 메모리 공간을 나타내며, 유튜브 영상의 '버퍼링 중입니다...' 할 때 그 임시 공간을 의미합니다.
| 종류 | 설명 | 예시 |
|---|---|---|
| Readable Stream | 데이터를 읽어들이기만 하는 스트림 (수도꼭지에서 물이 나오는 것) | fs.createReadStream() |
| Writable Stream | 데이터를 쓰기만 하는 스트림 (하수구로 물이 빠져나가는 것) | fs.createWriteStream() |
| Duplex Stream | 읽기와 쓰기가 동시에 가능한 양방향 스트림 | TCP Socket |
| Transform Stream | 읽은 데이터를 쓰기 전에 변환(압축, 암호화)하는 특별한 스트림 | zlib.createGzip() |
스트림끼리 연결할 때는 파이프를 연결하듯 readStream.pipe(writeStream) 형태로 작성합니다. 이렇게 하면 Node.js가 알아서 데이터를 읽고 쓰는 속도(Backpressure)를 조절해주어 메모리가 터지는 것을 방지해줍니다.
거대한 1GB 동영상 파일을 메모리에 한 번에 다 올리지 않고, 조각(Chunk/Buffer) 단위로 쪼개어 물 흐르듯 전송하는 Stream의 강력함을 확인하세요.
Express나 NestJS 같은 프레임워크를 사용하기 전에, Node.js가 웹 서버로서 어떻게 동작하는지 뼈대를 알아야 합니다. Node.js는 설치 즉시 http 모듈을 내장하고 있어 단 몇 줄의 코드만으로 전 세계에서 접속할 수 있는 웹 서버를 열 수 있습니다.
서버를 열기 위해 http.createServer() 함수를 호출합니다. 이 함수 안에는 클라이언트가 접속할 때마다 자동으로 실행되는 콜백 함수를 넣어주며, 이 함수는 항상 req와 res 두 개의 객체를 전달받습니다.
| 객체 | 역할 | 주요 속성 및 메서드 |
|---|---|---|
| req (Request) | 브라우저가 무엇을 원하는지(요청)가 담긴 택배 송장 같은 객체입니다. | req.url (접속 주소) req.method (GET, POST 등) |
| res (Response) | 서버가 브라우저에게 결과를 돌려줄 때(응답) 사용하는 택배 상자 같은 객체입니다. | res.writeHead() (상태코드) res.end() (전송 완료) |
순수 http 모듈만으로도 서버를 만들 수 있지만, 페이지 수가 10개, 100개로 늘어나면 if (req.url === '...') 구문이 수백 개로 늘어나 코드가 지저분해집니다. 이를 깔끔하게 정리해주고 보안, 파일 업로드 등 복잡한 기능을 쉽게 구현하게 해주는 것이 바로 Express.js 입니다.
외부 프레임워크 없이 내장 http 모듈만으로 서버를 띄우고, 접속한 클라이언트에게 응답(Response)을 보내는 과정입니다.
Express.js는 Node.js 위에서 동작하는 웹 프레임워크 중 전 세계 점유율 1위를 차지하는 사실상의 표준(De Facto Standard)입니다. 지저분한 라우팅 코드(if/else)를 직관적으로 분리해주며, 수천 개의 플러그인(미들웨어)을 장착할 수 있는 확장성을 자랑합니다.
미들웨어는 공장 컨베이어 벨트의 작업자들과 같습니다. 클라이언트의 요청이 들어오면, 1번 작업자(예: 로그 기록)가 일을 한 뒤 next()를 호출하여 2번 작업자(예: 권한 검사)에게 바통을 넘깁니다. 최종적으로 라우터가 res.send()로 클라이언트에 응답을 보내면 이 여정이 끝납니다.
클라이언트의 요청이 들어왔을 때, 여러 미들웨어를 거치며 순차적으로 처리된 후 최종 라우터에서 응답을 반환하는 과정을 확인해 보세요.
사용자가 어떤 주소(URL)로 들어왔느냐에 따라 다른 화면을 보여주는 것을 라우팅(Routing)이라고 합니다. Express는 app.get('/about', ...)처럼 매우 직관적인 메서드를 제공하여 수백 개의 URL도 깔끔하게 관리할 수 있습니다.
단순히 주소만 나누는 것이 아닙니다. 동일한 주소라도 '목적'에 따라 메서드를 다르게 받는 것이 REST API 설계의 기본입니다.
| 메서드 | 의미 (역할) | Express 문법 예시 |
|---|---|---|
| GET | 데이터를 조회(Read)할 때 사용. 브라우저에서 주소를 치고 들어가는 것은 무조건 GET입니다. | app.get('/posts', ...) |
| POST | 새로운 데이터를 생성(Create)할 때 사용. 로그인, 회원가입 폼 전송 시 쓰입니다. | app.post('/login', ...) |
| PUT / PATCH | 기존 데이터를 수정(Update)할 때 사용합니다. | app.put('/posts/:id', ...) |
| DELETE | 데이터를 삭제(Delete)할 때 사용합니다. | app.delete('/posts/:id', ...) |
req.params.id (결과: '123')req.query.keyword (결과: 'node')
URL 경로를 쪼개어 특정 변수를 추출하는 req.params와 URL 뒤에 붙는 검색어를 추출하는 req.query의 사용법입니다.
Express는 거대한 미들웨어(Middleware) 체인입니다. 미들웨어는 클라이언트의 요청(req)과 응답(res) 사이에 끼어들어(Middle) 조작을 가하는 함수입니다. 남이 만든 플러그인을 쓸 수도 있지만, 우리만의 로직을 담은 커스텀 미들웨어를 만들 줄 알아야 진정한 백엔드 개발자입니다.
| 역할 | 설명 및 예시 |
|---|---|
| 1. 데이터 조작/주입 | req 객체에 새로운 속성을 추가하여 다음 라우터로 넘겨줍니다. (예: req.user = 'Minstudio' 로 로그인 정보 주입) |
| 2. 요청 차단 (보안) | 로그인 안 한 유저면 next()를 호출하지 않고 곧바로 res.send('꺼져!') 로 응답을 끝내버립니다. 뒤에 있는 라우터는 실행되지 못합니다. |
| 3. 에러 처리로 넘기기 | 진행 중 DB 접속 실패 등 치명적인 에러가 발생하면 next(error) 형태로 넘깁니다. |
Express에서 매개변수가 정확히 4개 (err, req, res, next) 인 함수는 특별하게 에러 전용 미들웨어로 인식됩니다. 일반 미들웨어에서 next(err)를 호출하면, 중간에 있는 수많은 정상 미들웨어들은 모두 무시(Skip)하고 이 에러 전용 미들웨어로 즉시 순간이동(Jump) 합니다. 주로 app.js의 맨 마지막에 단 하나만 배치하여 모든 에러를 중앙 통제합니다.
미들웨어가 req 객체를 조작하여 뒤에 있는 라우터로 데이터를 넘겨주거나, 에러 발생 시 에러 처리 전용 미들웨어로 즉시 점프(Jump)하는 흐름입니다.
웹 서버는 기본적으로 해킹 방지를 위해 모든 폴더와 파일을 꽁꽁 숨겨둡니다. 클라이언트가 <img src="/logo.png">를 요청해도 서버가 허락하지 않으면 404 에러가 발생합니다. 이렇게 변하지 않는 파일(이미지, CSS, JS)을 누구나 접근할 수 있도록 특정 폴더를 개방(Public)하는 것이 바로 정적 파일 서빙(Static File Serving)입니다.
Express에서는 별도의 복잡한 코드 없이, 내장된 express.static 미들웨어 한 줄이면 특정 폴더를 즉시 외부에 공개할 수 있습니다.
| 서버 내 실제 파일 경로 | 브라우저 접속 주소 (URL) |
|---|---|
| public/logo.png | http://localhost:3000/logo.png |
| public/css/style.css | http://localhost:3000/css/style.css |
| public/js/main.js | http://localhost:3000/js/main.js |
가장 많이 하는 실수가 http://localhost:3000/public/logo.png 로 접속하는 것입니다. express.static으로 공개된 폴더는 그 폴더 자체가 루트 경로(/)가 되므로, public 이라는 폴더명은 URL에서 완전히 생략해야 합니다!
서버의 public 폴더를 외부에 공개하고, 브라우저에서 /logo.png 이미지에 직접 접속하는 과정입니다.
프론트엔드에서 백엔드로 데이터를 넘기는 방식은 크게 3가지가 있습니다. 주소에 몰래 숨기거나(Params), 꼬리표처럼 붙이거나(Query), 안전한 박스에 담아 보내는(Body) 방식입니다. Express는 이 3가지 데이터를 손쉽게 객체(Object) 형태로 뽑아낼 수 있게 해줍니다.
| 속성명 | 주 용도 | 데이터 전달 형태 |
|---|---|---|
| req.params | 특정 데이터 하나를 식별할 때 (유저 정보 보기, 특정 게시글 삭제 등) | /users/123 |
| req.query | 검색, 필터링, 정렬, 페이지네이션 (옵션 느낌의 데이터) | /search?keyword=node |
| req.body | 방대한 데이터, 보안이 필요한 데이터 (로그인 아이디/비번, 게시글 작성 등) | POST 전송 시 내부 Payload (JSON 등) |
req.params와 req.query는 Express가 자동으로 파싱(Parsing)해 주지만, req.body는 기본적으로 undefined를 반환합니다. 본문(Body) 데이터는 크기가 매우 클 수 있기 때문에, 서버 리소스 낭비와 보안 문제를 막기 위해 개발자가 "어떤 형식의 데이터를 받을 것인지" 명시적으로 허락(미들웨어 장착)해야만 읽을 수 있습니다.
데이터 파싱 미들웨어(express.json() 등)는 반드시 라우터(app.post(...))들보다 위쪽(코드 상단)에 작성해야 합니다. 미들웨어는 위에서 아래로 물 흐르듯 실행되므로, 미리 데이터를 파싱해 놓지 않으면 아래쪽의 라우터가 req.body를 읽지 못합니다.
클라이언트가 서버로 보내는 데이터의 3가지 핵심 방식이 Express 서버 측에서 어떻게 추출되어 객체로 변환되는지 확인하세요.
REST (Representational State Transfer)는 일종의 약속입니다. 수많은 개발자가 협업할 때 주소(URL)만 봐도 "아, 이건 유저 정보를 삭제하는 기능이구나!" 하고 한눈에 파악할 수 있도록 일관된 규칙으로 API를 설계하는 방법론을 말합니다.
| 원칙 | 설명 | ❌ 나쁜 예 | ✅ 좋은 예 |
|---|---|---|---|
| 1. 명사(Noun) 사용 | URL에 get, create 같은 동사를 쓰지 않습니다. 행동은 HTTP 메서드(GET, POST)가 책임집니다. |
/getUser | /users |
| 2. 복수형(Plural) 통일 | 자원의 이름은 통일성을 위해 가급적 단수보다는 복수형 명사로 통일합니다. | /post/1 | /posts/1 |
| 3. 계층 관계 표현 | 자원 간의 포함 관계는 슬래시(/)를 사용하여 상위/하위 개념을 표현합니다. |
/board-comment | /boards/2/comments |
동일한 /users 라는 주소라도 메서드에 따라 완전히 다른 로직이 수행됩니다.
동사(Verb)를 섞어 쓴 잘못된 설계와, 오직 명사(Noun)와 HTTP 메서드만으로 깔끔하게 역할을 분리한 좋은 설계를 비교해 보세요.
과거에는 서버(Node.js)가 직접 HTML 코드를 문자열로 덕지덕지 이어 붙여서(res.send('<h1>' + title + '</h1>')) 브라우저로 보냈습니다. 하지만 코드가 길어지면 유지보수가 불가능해집니다. 이를 해결하기 위해 HTML 뼈대 파일에 빈 칸을 뚫어놓고(Template), 서버가 그 빈 칸에 데이터를 채워 넣어(Render) 완성된 HTML을 찍어내는 기법이 바로 템플릿 엔진입니다.
Node.js 진영에서 가장 유명한 템플릿 엔진 중 하나입니다. HTML 태그 속에 특수한 기호(<% %>)를 열고 그 안에 진짜 자바스크립트 코드를 적을 수 있습니다.
| 문법 기호 | 역할 | 사용 예시 |
|---|---|---|
| <%= 변수명 %> | 값을 화면에 출력(렌더링)할 때 사용합니다. HTML 코드를 넣어도 텍스트로 이스케이프 처리하여 XSS 해킹을 방어합니다. | <h1><%= title %></h1> |
| <%- 변수명 %> | 값을 출력하되, 이스케이프 처리를 하지 않고 진짜 HTML 태그로 렌더링시킵니다. | <%- htmlContent %> |
| <% 로직 %> | 화면에 출력하지 않고, if문이나 for문 같은 자바스크립트 제어(로직)를 실행할 때 사용합니다. |
<% if (user) { %> 안녕! <% } %> |
클라이언트(브라우저)는 EJS 문법을 전혀 이해하지 못합니다. 브라우저로 응답이 넘어가기 직전에 Node.js 서버가 res.render() 함수를 통해 EJS 코드를 완벽한 순수 HTML 문자열로 변환(컴파일)해서 보내주기 때문에 화면이 렌더링될 수 있는 것입니다.
서버가 가진 변수(Data)를 HTML 뼈대(EJS)에 주입하여, 브라우저가 읽을 수 있는 순수 HTML로 조립해 내는 과정을 확인해 보세요.
Express 서버 자체는 휘발성 메모리(RAM)를 사용하기 때문에 껐다 켜면 회원 정보나 게시글이 모두 날아갑니다. 데이터를 영구적으로 보관하기 위해 MySQL이나 PostgreSQL 같은 관계형 데이터베이스(RDBMS)와 서버를 연결하는 방법을 알아봅니다.
Node.js가 DB와 대화하려면 통역사 역할을 하는 라이브러리(드라이버)를 NPM에서 설치해야 합니다.
| 데이터베이스 종류 | 설치 패키지 | 특징 |
|---|---|---|
| MySQL / MariaDB | npm install mysql2 | 과거의 mysql 모듈보다 빠르고, 기본적으로 Promise(async/await)를 완벽 지원합니다. |
| PostgreSQL | npm install pg | PostgreSQL 공식 드라이버로 안정성이 뛰어납니다. |
DB 서버와 처음 연결(Handshake)을 맺는 과정은 아주 느리고 무거운 작업입니다. 만약 사용자 100명이 동시에 접속할 때마다 새로 연결을 맺고 끊으면 서버는 뻗어버립니다.
해결책은 커넥션 풀(Pool)입니다. 서버를 켤 때 DB와의 연결 선을 미리 10개~50개 만들어 수영장(Pool)에 담아두고, 사용자가 오면 연결 선을 빌려주고 작업이 끝나면 다시 돌려받아 재사용합니다. 실무 백엔드 개발에서 풀 사용은 선택이 아닌 필수입니다.
요청이 올 때마다 DB에 연결(Connect)하고 끊는 방식과, 미리 연결을 여러 개 만들어두고 돌려쓰는 풀(Pool) 방식의 코드를 비교해 보세요.
MongoDB는 대표적인 NoSQL 데이터베이스입니다. 복잡한 표(Table) 형태가 아니라, 자바스크립트 객체(JSON)와 똑같이 생긴 도큐먼트(Document) 형태로 데이터를 저장합니다. 형식이 자유로워서 프론트엔드 개발자들이 적응하기 매우 쉽습니다.
데이터를 다루는 방식은 비슷하지만 부르는 이름이 다릅니다. 이 차이를 이해하는 것이 MongoDB 학습의 첫걸음입니다.
| 개념 | MySQL (RDBMS) | MongoDB (NoSQL) |
|---|---|---|
| 데이터 1건 | Row (행/레코드) | Document (도큐먼트) |
| 데이터 집합(그룹) | Table (테이블) | Collection (컬렉션) |
| 고유 식별자(기본키) | Primary Key (id) | _id (자동 생성되는 난수 문자열) |
MongoDB는 너무 자유로워서 숫자 자리에 문자를 넣거나 필수 항목을 빼먹고 저장해도 오류가 나지 않습니다. 이런 데이터 오염을 막기 위해 Mongoose (몽구스)라는 ODM 라이브러리를 사용합니다.
스키마(Schema)는 단순히 데이터의 형태를 정의한 '설계도'일 뿐입니다. 이 설계도를 가지고 실제 DB에 데이터를 넣고(save), 찾고(find) 지우려면 mongoose.model('User', userSchema) 를 통해 조종석 격인 모델(Model) 객체로 변환해야 합니다.
아무 데이터나 다 받아주는 MongoDB의 자유로움을 통제하기 위해, Mongoose 스키마(Schema)가 불량 데이터를 어떻게 막아내는지 확인해 보세요.
데이터베이스 비밀번호, API 결제 키, JWT 시크릿 키 등을 자바스크립트 코드 파일(app.js)에 직접 적어두는 것(하드코딩)은 자살 행위와 같습니다. Github에 코드를 올리는 순간 전 세계 해커들의 봇(Bot)이 1초 만에 스크래핑하여 해킹을 시도합니다. 이를 막기 위해 코드와 비밀번호를 완전히 분리하는 기술이 바로 환경 변수(Environment Variables)입니다.
Node.js에서는 dotenv라는 아주 유명한 패키지를 사용하여 이 문제를 해결합니다. 서버 컴퓨터(또는 로컬 PC)에만 .env라는 금고 파일을 몰래 만들어두고, 코드에서는 그 금고 안의 내용물만 꺼내어 쓰는 방식입니다.
아무리 .env 파일로 잘 분리했다 하더라도, .env 파일 자체가 Github에 올라가버리면 아무 소용이 없습니다! 프로젝트 폴더에 있는 .gitignore 파일을 열어서 .env라는 글자를 반드시 추가해야 합니다. 그래야 Git이 .env 파일을 무시하고 업로드하지 않습니다.
코드 파일(app.js)에 비밀번호를 직접 적지 않고, 로컬에만 존재하는 .env 금고 파일에 보관하여 해킹을 방지하는 원리를 확인해 보세요.
비동기 함수 안에서 에러가 발생했는데 try-catch로 잡아주지 않으면 Node.js 서버 프로세스가 통째로 죽어버리는(Crash) 대형 사고가 발생합니다. 이를 방지하기 위한 최후의 보루이자 우아한 에러 처리 패턴이 바로 전역 에러 미들웨어입니다.
Express에서 전역 에러 처리 미들웨어로 인식되기 위해서는 반드시 4개의 인자 (err, req, res, next)를 가져야 합니다. 만약 next를 생략해서 인자가 3개가 되면, 일반 미들웨어로 취급되어 에러를 잡지 못합니다.
| 속성 (Property) | 타입 | 설명 | 사용 예시 |
|---|---|---|---|
err.message |
String | 에러에 대한 간단한 텍스트 설명 | "User not found" |
err.stack |
String | 에러가 발생한 호출 스택(디버깅 용) | 서버 로그 기록용으로 사용 |
err.statusCode (커스텀) |
Number | 명시적으로 지정한 HTTP 상태 코드 | 400, 401, 404 |
클라이언트의 요청 타입에 따라 고의로 에러를 발생시키고, 하나의 미들웨어(전역 에러 핸들러)가 모든 에러를 일괄적으로 잡아내어 안전하게 응답하는 과정을 확인해 보세요.
HTTP는 본래 "기억력"이 없어서(Stateless), 방금 로그인한 유저도 새로고침하면 누구인지 까먹습니다. 이를 해결하기 위해 서버(세션)에 방문자 명부를 적어두고, 브라우저에게 입장권(쿠키)을 쥐여주는 것이 전통적인 인증 방식입니다.
- 세션(Session): 놀이공원 매표소의 장부 (누가 입장권을 샀는지 서버가 기억함)
- 쿠키(Cookie): 손님(브라우저)의 손목에 채워주는 자유이용권 팔찌 (요청할 때마다 서버에 보여줌)
| 구분 | 쿠키 (Cookie) | 세션 (Session) |
|---|---|---|
| 저장 위치 | 클라이언트 (브라우저) | 서버 (메모리 또는 DB) |
| 보안성 | 취약함 (탈취 및 변조 가능) | 비교적 우수함 (데이터는 서버에 있음) |
| 속도 / 부하 | 빠름 / 서버 부하 적음 | 조금 느림 / 사용자 많을시 서버 부하 큼 |
서버가 어떻게 세션을 발급하여 기억하고, 브라우저가 어떻게 쿠키를 매번 제출하여 자신이 누구인지 증명하는지 확인해 보세요.
세션 방식은 접속자가 10만 명이 되면 서버 메모리(장부)가 터진다는 치명적인 단점이 있습니다. 이를 극복하기 위해 나온 기술이 바로 토큰 기반 무상태(Stateless) 인증 기법입니다.
경찰이 시민의 신원을 확인할 때마다 경찰청 서버(세션)에 일일이 조회하지 않듯, 위조 불가능한 홀로그램 서명이 들어간 신분증(JWT)을 시민(브라우저)이 스스로 들고 다니며 요청마다 보여주는 방식입니다.
| 구분 | 역할 | 내용 예시 |
|---|---|---|
| Header (헤더) | 어떤 알고리즘으로 암호화했는지 명시 | { "alg": "HS256", "typ": "JWT" } |
| Payload (페이로드) | 유저 ID, 권한 등 전달할 실제 데이터 조각(Claim) | { "userId": 1, "role": "admin" } |
| Signature (서명) | 가장 중요한 부분! 서버의 비밀키로 생성한 위조 방지 도장 | HMACSHA256(base64(Header) + base64(Payload), SECRET_KEY) |
서버가 어떻게 세션 DB 없이도 서명(Signature)만으로 사용자를 식별하고, 클라이언트가 발급받은 JWT 토큰을 어떻게 보관하여 사용하는지 확인해 보세요.
웹 서비스 런칭 전 반드시 점검해야 할 보안 수칙들입니다. DB가 털리더라도 유저의 원래 비밀번호를 절대 알아낼 수 없게 만드는 단방향 암호화(Bcrypt) 기법과, 출처가 다른 악성 도메인에서 들어오는 API 요청을 원천 차단하는 CORS 방어벽 설정입니다.
- Bcrypt 해싱: 딸기(비밀번호)를 믹서기에 넣고 갈아버리는 것과 같습니다. 딸기 주스(해시)를 보고 원래 딸기 모양을 복원할 수 없는 단방향 암호화입니다.
- CORS: 클럽 가드가 손님의 신분증을 보고 "우리 클럽 앱에서 온 요청 맞나요? 아니면 쫓아냅니다" 라고 출처(Origin)를 검사하는 것입니다.
| 용어 | 설명 | 비유 |
|---|---|---|
| 단방향 암호화 (Hash) | A를 B로 바꿀 순 있지만, B를 보고 A가 뭔지 역추적하는 것이 수학적으로 불가능한 알고리즘 | 딸기(A)를 믹서기에 갈아 주스(B)로 만들기 |
| 솔트 (Salt) | 해시 함수의 결과값이 항상 같으면 패턴을 유추할 수 있으므로, 원본 암호에 무작위 문자열(소금)을 추가해 갈아버리는 기법 | 딸기 주스에 랜덤한 맛의 조미료 넣기 (해커 혼란용) |
| 라운드 (Cost Factor) | 해싱을 몇 번 반복할 것인지 결정. 10이면 2^10(1024번) 반복. 해커의 무차별 대입(Brute Force) 공격 시간을 지연시킴 | 믹서기를 1000번 반복해서 돌리기 |
클라이언트의 출처(Origin)에 따라 서버가 CORS 차단을 수행하는 과정과, 비밀번호가 Bcrypt 알고리즘을 통해 복구 불가능한 해시로 변환되는 과정을 확인해 보세요.
텍스트나 JSON과 달리 이미지, 영상 같은 미디어 파일(multipart/form-data)은 크기가 커서 잘게 쪼개져서 전송됩니다. 일반적인 express.json()으로는 이를 파싱할 수 없으며, Multer라는 전용 미들웨어가 조각난 파일을 하나로 합쳐 하드디스크나 클라우드에 안전하게 저장해 줍니다.
클라이언트가 10GB짜리 영상을 한 번에 던지면 서버 메모리가 터집니다. 그래서 수천 개의 작은 조각(퍼즐)으로 쪼개서 보냅니다. Multer는 이 퍼즐 조각들이 도착하는 족족 조립하여 하드디스크에 "완성된 파일"로 저장해주는 컨베이어 벨트 역할을 합니다.
| 옵션 | 특징 | 사용처 |
|---|---|---|
| DiskStorage | 서버의 하드디스크(예: uploads/)에 물리적인 파일로 즉시 저장함 |
일반적인 로컬 서버 개발, 소규모 프로젝트 |
| MemoryStorage | 파일을 디스크에 쓰지 않고 RAM(메모리)에 Buffer 객체로 들고 있음 | 곧바로 AWS S3 등 클라우드로 토스해야 할 때 |
클라이언트가 폼 데이터로 전송한 큰 파일을 서버의 Multer 미들웨어가 어떻게 받아 디스크에 저장하는지 확인해 보세요.
클라이언트(사용자)가 보낸 데이터는 절대 믿으면 안 됩니다. 나이를 실수로 문자로 보내거나 이메일 형식이 아닌 엉뚱한 데이터를 DB에 넣으려다 서버가 터지는 것을 막기 위해, 백엔드 컨트롤러 입구에서 데이터를 깐깐하게 검문하는 필수 절차입니다. 주로 Joi 라이브러리를 사용합니다.
클럽 가드(서버)가 미리 "성인일 것, 슬리퍼 금지, 마스크 착용" 이라는 체크리스트(Schema)를 들고 있습니다. 손님(Data)이 들어올 때 이 체크리스트와 대조해서 하나라도 어긋나면 입구컷 (400 Bad Request)을 시켜버리는 원리입니다.
| 메서드 | 설명 | 예시 |
|---|---|---|
| .string() / .number() | 데이터의 기본 자료형 검사 | Joi.number() |
| .min(n) / .max(n) | 글자 수 또는 숫자의 최소/최대 길이 지정 | Joi.string().min(4) |
| .required() | 반드시 제출해야 하는 필수 값 지정 (없으면 에러) | Joi.string().required() |
| .email() / .pattern(RegExp) | 정확한 이메일 형식인지, 혹은 특정 정규식에 맞는지 패턴 검사 | Joi.string().email() |
클라이언트가 잘못된 데이터를 보냈을 때 백엔드 스키마가 어떻게 걸러내고 400 Bad Request를 띄우는지 테스트해 보세요.
기존 HTTP 통신은 클라이언트가 "요청"해야만 서버가 "응답"하고 전화가 뚝 끊어지는 단방향 구조입니다. 반면 웹소켓(Socket.io)은 서버와 클라이언트가 연결 터널을 계속 열어둔 채 통화 상태를 유지하며, 서버가 원할 때 언제든 클라이언트에게 카톡 알림이나 주식 호가처럼 데이터를 실시간으로 밀어 넣어줄 수 있는 강력한 양방향 통신 기술입니다.
- HTTP: 편지. 답장을 받으려면 무조건 내가 먼저 편지를 써서 우체통에 넣어야 합니다.
- 웹소켓: 전화통화. 한 번 전화를 걸어서 연결해두면 끊기 전까지 서로 언제든 말을 걸고 대답할 수 있습니다.
| 메서드 | 설명 | 비유 |
|---|---|---|
| .on('이벤트', 콜백) | 특정 이름의 이벤트가 도착할 때까지 기다렸다가 수신 | 무전기 수신 대기 (귀를 열고 듣기) |
| .emit('이벤트', 데이터) | 나와 연결된 대상에게 이벤트를 발생시키며 데이터 전송 | 무전기 발신 (말하기) |
| io.emit() | 현재 접속 중인 모든 클라이언트에게 데이터 전송 | 확성기로 전체 방송하기 |
| socket.broadcast.emit() | 나(발신자)를 제외한 나머지 모든 사람에게 전송 | "나 빼고 다 들어~!" |
서버가 어떻게 클라이언트와의 연결을 유지하고 데이터를 실시간으로 밀어넣어 주는지(Push) 그룹 채팅 예제로 확인해 보세요.
실제 배포된 운영 서버(Production)에서는 절대 console.log()를 쓰지 않습니다! 서버 컴퓨터를 재부팅하거나 터미널 창을 닫으면 그동안 찍혔던 에러 로그가 공중으로 다 날아가 버리기 때문입니다. 나중에 해커가 공격했거나 치명적인 버그가 터졌을 때 증거를 남기기 위해 파일 형태로 꼼꼼하게 일기(Log)를 쓰는 기술이 필요합니다.
- Morgan: 식당 문 앞에 설치된 CCTV. "누가 몇 시에 어떤 문 열고 들어왔다 나감(상태코드 200)"을 꼬박꼬박 기록합니다.
- Winston: 비행기 블랙박스. 시스템 내부 깊숙한 곳에서 심각한 엔진 고장(Error)이 났을 때 영구적인 하드디스크 파일(`error.log`)로 보존합니다.
| 레벨 (중요도) | 의미 | 예시 |
|---|---|---|
| error (0) | 치명적인 시스템 오류. 개발자에게 즉시 알람이 가야 함 | "DB 접속 실패", "결제 모듈 다운" |
| warn (1) | 에러는 아니지만 주의가 필요한 이상 현상 | "메모리 사용량 80% 돌파", "비밀번호 5회 틀림" |
| info (2) | 정상적인 서비스 흐름에 대한 중요한 기록 | "홍길동님이 로그인함", "새 게시글 작성됨" |
| debug (3) | 개발 과정에서 문제 원인을 파악하기 위해 찍어보는 상세 정보 | "변수 x의 현재 값: 42" |
서버 터미널에 임시로 찍히는 로그(Morgan)와, 에러 발생 시 영구적으로 하드디스크에 저장되는 파일 로그(Winston)의 차이를 확인해 보세요.
새로운 기능을 추가하거나 남의 코드를 고쳤을 때, "기존에 잘 되던 결제 기능이나 로그인 기능이 망가지지 않았을까?" 걱정하게 됩니다. 이때 사람이 일일이 클릭해보는 대신, 똑똑한 검사 로봇(Jest)을 시켜서 단 1초 만에 수백 개의 기능을 자동으로 검사(Test)하여 초록색(PASS)이나 빨간색(FAIL) 도장을 찍게 만드는 실무 최고의 보험입니다.
- 수동 테스트(Manual): 공장장이 직접 장난감(함수)을 하나하나 작동시켜보고 불량인지 확인합니다. (매우 느리고 실수함)
- 자동화 테스트(Jest): 컨베이어 벨트에 장난감을 올리면, 검수 로봇이 "팔이 움직이나? 소리가 나나?" 순식간에 100가지를 검사하고 합격(Pass) 스티커를 붙입니다.
| 문법(Matcher) | 용도 | 예시 |
|---|---|---|
| .toBe(정답) | 단순한 값(숫자, 문자열 등)이 완벽히 일치하는지 검사 | expect(1 + 2).toBe(3) |
| .toEqual(객체) | 객체나 배열 안의 내용물이 전부 똑같이 생겼는지 검사 | expect({a:1}).toEqual({a:1}) |
| .toBeTruthy() | 결과가 True 계열인지 검사 (존재하는지 등) | expect(isLogin).toBeTruthy() |
| .toThrow() | 실행했을 때 의도한 대로 에러가 빵 터지는지 검사 | expect(badFunc).toThrow() |
사람이 브라우저에서 버튼을 누를 필요 없이, Jest 명령어를 실행하면 작성된 Test Suite 코드들이 로봇처럼 함수를 대신 실행해 보고 정답을 채점해 줍니다.