Node.js 시작하기 | 설치, 설정, Hello World
이 글의 핵심
Node.js 시작하기: 설치, 설정, Hello World. Node.js vs 브라우저 JavaScript·Node.js 설치.
들어가며
Node.js란?
Node.js는 Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임 환경입니다. 브라우저 밖에서 JavaScript를 실행할 수 있게 해줍니다. 이벤트 루프를 한 가지로 비유하면, 식당에서 주문을 받는 직원 한 명이 테이블마다 “음식 나오면 알려 드릴게요”라고 번호를 남기고 다음 손님을 받는 것과 비슷합니다. 파일·네트워크처럼 시간이 걸리는 일은 주방(운영체제·스레드 풀)에 맡겨 두고, 끝나면 등록해 둔 콜백을 순서에 맞춰 처리합니다. 우체국에서 소포를 부치고 번호표를 받아 나중에 찾으러 오는 흐름으로 이해하셔도 됩니다. 주요 특징:
- ✅ 비동기 I/O(한 작업이 끝나기를 기다리지 않고 다음 일을 넘기는 입출력): Non-blocking I/O로 높은 성능
- ✅ 단일 스레드: 이벤트 루프 기반
- ✅ 크로스 플랫폼: Windows, macOS, Linux 지원
- ✅ npm: 세계 최대 패키지 저장소
- ✅ JavaScript: 프론트엔드와 같은 언어 사용 Node.js가 적합한 경우:
- ✅ REST API 서버
- ✅ 실시간 애플리케이션 (채팅, 게임)
- ✅ 마이크로서비스
- ✅ CLI 도구
- ✅ 스트리밍 서비스 Node.js가 부적합한 경우:
- ❌ CPU 집약적 작업 (이미지/비디오 처리)
- ❌ 복잡한 계산 작업
아키텍처 심화: V8 통합과 libuv
입문 편에서도 런타임을 한 장의 그림으로 잡아 두면 이후 모듈·비동기·스트림 글이 모두 같은 좌표계 위에 올라갑니다. 여기서는 Node.js가 어떤 경계로 V8과 libuv를 붙이는지를 엔지니어막 관점에서 정리합니다.
V8 통합: 자바스크립트 실행은 여기서 끝난다
V8은 ECMAScript를 실행하는 엔진으로, 파싱 → 바이트코드(이그니션 Ignition) → 최적화 기계어(터보팬 TurboFan) 같은 파이프라인과 가비지 컬렉션(GC) 을 포함합니다. Node.js는 V8을 “임베딩”하는 쪽에 가깝습니다. 즉, 브라우저가 아니라 서버용 런타임이 V8 위에 자신만의 C++ 레이어를 얹는 구조입니다.
Node가 V8과 맺는 대표적인 계약은 다음과 같습니다.
- Isolate·컨텍스트(Context): Isolate는 V8의 힙·GC 경계이자 실행 단위에 가깝고, 컨텍스트는 전역 객체·내장 객체가 살아 있는 JS 세계입니다. Node는 기동 시 이들을 준비하고,
globalThis·내장 모듈 바인딩·네이티브 애드온 로딩을 이 레이어에 연결합니다. - 내장 API와의 바인딩:
fs.readFile같은 함수는 JavaScript 표면에서 호출되지만, 실제 파일 디스크립터·버퍼 할당·에러 코드 매핑은 C++ 바인딩을 거칩니다. 이 경계에서 V8의 핸들(Handle)·스코프(Scope) 규칙이 적용되어, 네이티브 객체 수명과 JS 객체 수명이 어긋나지 않게 맞춥니다. - 힙·GC와의 공존: 대량 버퍼·소켓·스트림은 네이티브 쪽 메모리를 많이 쓰는데, 이는 V8 힙 밖에 존재할 수 있습니다. 따라서 “JS만 보면 메모리가 작아 보이는데 RSS는 큰” 현상이 생기며, 이는 V8 GC와 libuv 버퍼·소켓 버퍼가 별도 계정이기 때문입니다.
정리하면, 사용자 코드의 동기 실행·객체 생성·프로토타입 체인 해석은 V8의 영역이고, OS 자원·타이밍·스레드는 다음 절의 libuv(와 운영체제) 쪽에서 다룹니다.
libuv: 이벤트 루프·스레드 풀·크로스 플랫폼 I/O
libuv는 Node의 심장부에 가까운 C 라이브러리로, 이벤트 루프 구현, 비동기 I/O를 위한 OS 추상화(epoll·kqueue·IOCP 등), 스레드 풀을 제공합니다. “Node는 싱글 스레드”라는 말은 자바스크립트 실행 관점에 가깝고, 디스크·DNS·일부 암호 연산은 libuv가 별도 스레드 풀로 처리할 수 있습니다.
- 이벤트 루프와 큐: 타이머·I/O 완료·체크 단계 등은 libuv의 루프가 한 턴씩 돌며, 완료된 작업에 대응하는 콜백이 V8이 다시 JS를 실행할 수 있는 형태로 스케줄됩니다. 이 “한 턴”의 세부 순서는 비동기 프로그래밍 편에서 단계별로 다룹니다.
- 핸들(Handle)·요청(Request): 소켓·타이머·파이프 같은 장수명 객체는 libuv 핸들로 표현되고, 파일 읽기 같은 일회성 작업은 요청 객체로 큐에 넣어 스레드 풀 워커가 처리합니다. 완료 시 콜백이 메인 루프로 돌아옵니다.
- 스레드 풀의 역할:
fs의 일부 작업,crypto의 일부 연산,dns.lookup등은 논블로킹 API가 없거나 Node가 안전하게 스레드로 빼는 편이 나은 경로로 분류됩니다. 반면 TCP 소켓 기반 네트워크는 대체로 루프의 폴링(poll) 단계와 직접 연결됩니다. - 크로스 플랫폼: Windows의 IOCP와 유닉스 계열의 epoll/kqueue 차이를 libuv가 흡수하므로, 동일한 Node API가 플랫폼마다 다른 최적 경로를 타게 됩니다.
한 줄로 보는 호출 경로
대표적인 흐름을 요청 → 완료 순으로만 적으면 다음과 같습니다.
- JS에서
fs.promises.readFile호출 - C++ 바인딩이 버퍼·경로·플래그를 준비하고 libuv에 파일 읽기 요청을 등록
- 워커 스레드(또는 비동기 경로)가 디스크/캐시에서 데이터를 읽음
- 완료 시 메인 스레드의 이벤트 루프가 콜백·Promise 후속을 실행
- V8이 다시 JS 스택을 쌓아 사용자 코드의
await이후를 진행
이 그림을 알고 있으면, “왜 CPU를 태우는 순수 연산은 스레드 풀도 막아 주지 않는가”, “왜 소켓은 빠른데 디스크는 상황에 따라 밀리는가” 같은 질문에 바로 답이 잡힙니다.
실전 경험에서 배운 교훈
이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.
가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.
이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.
1. Node.js vs 브라우저 JavaScript
비교
| 특징 | 브라우저 JavaScript | Node.js |
|---|---|---|
| 실행 환경 | 브라우저 (Chrome, Firefox) | 서버, 로컬 머신 |
| 전역 객체 | window | global |
| DOM 접근 | ✅ | ❌ |
| 파일 시스템 | ❌ | ✅ (fs 모듈) |
| HTTP 서버 | ❌ | ✅ (http 모듈) |
| 모듈 시스템 | ES Modules | CommonJS + ES Modules |
| 패키지 관리 | 없음 | npm, yarn |
공통 기능
브라우저와 Node.js 모두에서 사용할 수 있는 JavaScript 표준 기능들입니다:
// 콘솔 출력 - 디버깅과 로깅에 사용
console.log("Hello");
// 타이머 함수 - 일정 시간 후 코드 실행
setTimeout(() => {
console.log("1초 후 실행");
}, 1000);
// 반복 타이머 - 일정 간격으로 코드 반복 실행
setInterval(() => {
console.log("1초마다 실행");
}, 1000);
// 비동기 처리 - Promise와 async/await 문법
const fetchData = async () => {
const result = await Promise.resolve("데이터");
return result;
};
// JSON 파싱 - 문자열을 객체로, 객체를 문자열로 변환
const obj = JSON.parse('{"name": "홍길동"}');
const str = JSON.stringify({ name: "홍길동" });
브라우저와 Node.js 모두 같은 ECMAScript 문법으로 Promise와 async/await를 씁니다. JSON.parse와 JSON.stringify는 API 응답·.env를 다룰 때 문자열과 객체를 오갈 때마다 사용합니다.
Node.js 전용 기능
Node.js에서만 사용할 수 있는 서버 사이드 기능들입니다:
// 파일 시스템 - 파일 읽기/쓰기 기능
const fs = require('fs');
// readFileSync: 파일을 동기적으로 읽음 (파일을 다 읽을 때까지 대기)
// 'utf8': 텍스트 인코딩 지정 (한글 등 문자 처리)
const content = fs.readFileSync('file.txt', 'utf8');
// HTTP 서버 - 웹 서버 생성 기능
const http = require('http');
// createServer: 서버 인스턴스 생성
// req: 클라이언트 요청 객체, res: 서버 응답 객체
const server = http.createServer((req, res) => {
res.end('Hello'); // 응답 전송 후 연결 종료
});
// 경로 처리 - 파일 경로를 안전하게 조작
const path = require('path');
// __dirname: 현재 스크립트가 있는 디렉토리의 절대 경로
// join: 경로를 OS에 맞게 결합 (Windows: \, Linux: /)
const filePath = path.join(__dirname, 'file.txt');
// 프로세스 정보 - 현재 실행 중인 Node.js 프로세스 정보
console.log(process.version); // Node.js 버전 (예: v20.11.0)
console.log(process.platform); // 운영체제 (예: win32, darwin, linux)
console.log(process.cwd()); // 현재 작업 디렉토리
console.log(process.pid); // 프로세스 ID
fs는 비동기(readFile)와 동기(readFileSync)를 둘 다 제공합니다. 요청을 처리하는 콜백 안에서 readFileSync를 쓰면 그동안 다른 HTTP 요청도 기다리게 되므로, 서버 코드에서는 비동기 API가 기본입니다. path.join은 Windows의 \와 POSIX의 /를 섞지 않도록 경로를 이어 줍니다.
2. Node.js 설치
Windows
- nodejs.org 방문
- LTS 버전 다운로드 (안정 버전, 권장)
- 설치 프로그램 실행
- 설치 확인:
node --version
# v20.11.0
npm --version
# 10.2.4
macOS
방법 1: 공식 설치 프로그램
# nodejs.org에서 다운로드
방법 2: Homebrew
brew install node
# 버전 확인
node --version
npm --version
Linux (Ubuntu/Debian)
# NodeSource 저장소 추가
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# 설치
sudo apt-get install -y nodejs
# 버전 확인
node --version
npm --version
버전 관리 (nvm)
여러 Node.js 버전을 관리하려면 nvm 사용:
# nvm 설치 (Linux/macOS)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Node.js 설치
nvm install 20
nvm install 18
# 버전 전환
nvm use 20
nvm use 18
# 현재 버전
nvm current
# 설치된 버전 목록
nvm list
3. 첫 Node.js 프로그램
Hello World
// hello.js
console.log("Hello, Node.js!");
// 현재 Node.js 버전
console.log(`Node.js 버전: ${process.version}`);
// 플랫폼 정보
console.log(`플랫폼: ${process.platform}`);
실행:
node hello.js
# Hello, Node.js!
# Node.js 버전: v20.11.0
# 플랫폼: win32
명령줄 인자
// args.js
console.log("명령줄 인자:", process.argv);
// process.argv[0]: node 실행 파일 경로
// process.argv[1]: 스크립트 파일 경로
// process.argv[2~]: 사용자 인자
const args = process.argv.slice(2);
console.log("사용자 인자:", args);
if (args.length === 0) {
console.log("사용법: node args.js <이름>");
process.exit(1);
}
const name = args[0];
console.log(`안녕하세요, ${name}님!`);
실행:
node args.js 홍길동
# 안녕하세요, 홍길동님!
환경 변수
// env.js
console.log("환경 변수:", process.env);
// 특정 환경 변수
const port = process.env.PORT || 3000;
const nodeEnv = process.env.NODE_ENV || 'development';
console.log(`포트: ${port}`);
console.log(`환경: ${nodeEnv}`);
실행:
# Windows
set PORT=8080 && node env.js
# Linux/macOS
PORT=8080 node env.js
4. 첫 HTTP 서버
기본 서버
가장 간단한 형태의 HTTP 서버를 작성해 봅니다.
// server.js
const http = require('http');
// createServer: 서버 인스턴스 생성
// 콜백 함수는 요청이 올 때마다 실행됨
const server = http.createServer((req, res) => {
// req.method: HTTP 메서드 (GET, POST 등)
// req.url: 요청된 URL 경로 (예: /, /about)
console.log(`${req.method} ${req.url}`);
// writeHead: HTTP 상태 코드와 헤더 설정
// 200: 성공 응답
// Content-Type: 응답 데이터 형식 지정
// charset=utf-8: 한글 등 유니코드 문자 처리
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
// end: 응답 본문을 전송하고 연결 종료
res.end('안녕하세요, Node.js 서버입니다!');
});
// 서버를 특정 포트에서 실행
const PORT = 3000;
server.listen(PORT, () => {
// 서버가 시작되면 이 콜백이 실행됨
console.log(`서버가 http://localhost:${PORT} 에서 실행 중`);
});
코드 흐름:
http모듈을 불러옵니다.createServer에 요청이 올 때마다 호출될 함수를 넘깁니다(콜백).listen으로 포트 3000에서 연결을 받을 준비를 합니다.- 브라우저 등이 접속하면
req·res가 채워진 채로 위 콜백이 실행됩니다. writeHead·end로 상태 코드·본문을 보내고 응답을 마칩니다. 실행:
node server.js
# 서버가 http://localhost:3000 에서 실행 중
브라우저에서 http://localhost:3000 접속하면 메시지가 표시됩니다.
라우팅 추가
URL 경로에 따라 다른 응답을 반환하는 라우팅 기능을 구현합니다:
// server-routing.js
const http = require('http');
const server = http.createServer((req, res) => {
// 구조 분해로 요청 메서드와 URL 추출
const { method, url } = req;
// 모든 응답에 공통으로 적용할 헤더 설정
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// URL과 메서드에 따라 분기 처리
if (url === '/' && method === 'GET') {
// 루트 경로 - 홈 페이지
res.writeHead(200); // 200: OK 상태 코드
res.end('홈 페이지');
} else if (url === '/about' && method === 'GET') {
// /about 경로 - 소개 페이지
res.writeHead(200);
res.end('소개 페이지');
} else if (url === '/api/users' && method === 'GET') {
// API 엔드포인트 - JSON 응답
res.writeHead(200, { 'Content-Type': 'application/json' });
// JavaScript 객체를 JSON 문자열로 변환하여 응답
res.end(JSON.stringify({ users: ['홍길동', '김철수'] }));
} else {
// 매칭되는 경로가 없으면 404 에러
res.writeHead(404); // 404: Not Found 상태 코드
res.end('페이지를 찾을 수 없습니다');
}
});
server.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
라우팅 동작 원리:
- 클라이언트가 특정 URL로 요청
req.url과req.method를 확인- 조건문으로 매칭되는 경로 찾기
- 해당 경로에 맞는 응답 반환
- 매칭되는 경로가 없으면 404 에러 테스트:
# 브라우저 또는 curl로 테스트
curl http://localhost:3000/
curl http://localhost:3000/about
curl http://localhost:3000/api/users
HTML 응답
// server-html.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Node.js 서버</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
h1 { color: #68a063; }
</style>
</head>
<body>
<h1>Node.js 서버에 오신 것을 환영합니다!</h1>
<p>현재 시간: ${new Date().toLocaleString('ko-KR')}</p>
<p>요청 URL: ${req.url}</p>
<p>요청 메서드: ${req.method}</p>
</body>
</html>
`;
res.end(html);
});
server.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
5. npm (Node Package Manager)
package.json 생성
# 프로젝트 폴더 생성
mkdir my-node-project
cd my-node-project
# package.json 생성 (대화형)
npm init
# package.json 생성 (기본값 사용)
npm init -y
생성된 package.json:
{
"name": "my-node-project",
"version": "1.0.0",
"description": "Node.js 프로젝트",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
패키지 설치
# 프로젝트 의존성 설치
npm install express
# 여러 패키지 동시 설치
npm install express body-parser cors
# 개발 의존성 설치 (배포 시 제외)
npm install --save-dev nodemon eslint
# 전역 설치
npm install -g nodemon
# 특정 버전 설치
npm install [email protected]
package.json 업데이트:
{
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
패키지 관리
# 설치된 패키지 목록
npm list
# 패키지 업데이트
npm update
# 패키지 제거
npm uninstall express
# 보안 취약점 검사
npm audit
# 취약점 자동 수정
npm audit fix
# 캐시 정리
npm cache clean --force
package.json scripts
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"lint": "eslint .",
"build": "webpack"
}
}
실행:
npm start # node index.js
npm run dev # nodemon index.js
npm test # jest
6. 모듈 시스템
CommonJS (기본)
Node.js의 기본 모듈 시스템입니다. 코드를 재사용 가능한 모듈로 분리할 수 있습니다. 내보내기 (module.exports):
// math.js - 수학 관련 함수들을 모듈로 만들기
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
const PI = 3.14159;
// 방법 1: 객체로 내보내기 (권장)
// module.exports에 객체를 할당하면 해당 객체가 모듈의 공개 API가 됨
module.exports = {
add, // add: add와 동일 (ES6 단축 속성)
subtract,
PI
};
// 방법 2: 개별 내보내기
// exports 객체에 속성을 추가하는 방식
// 주의: exports = {}는 작동하지 않음 (참조가 끊김)
exports.add = add;
exports.subtract = subtract;
exports.PI = PI;
가져오기 (require):
// app.js - math 모듈 사용하기
// require: 모듈을 불러와서 module.exports 객체를 반환
// './math': 상대 경로 (./ = 현재 디렉토리)
const math = require('./math');
// 모듈의 함수와 변수 사용
console.log(math.add(10, 5)); // 15
console.log(math.subtract(10, 5)); // 5
console.log(math.PI); // 3.14159
// 구조 분해 할당 - 필요한 것만 추출
// 코드가 더 간결해지고 가독성이 향상됨
const { add, subtract } = require('./math');
console.log(add(10, 5)); // 15 - math. 접두사 없이 바로 사용
모듈 캐싱:
require는 모듈을 처음 불러올 때만 실행하고, 이후에는 캐시된 결과를 반환- 같은 모듈을 여러 번
require해도 한 번만 실행됨 - 모듈은 싱글톤 패턴처럼 동작
ES Modules (최신)
package.json 설정:
{
"type": "module"
}
내보내기 (export):
// math.mjs (또는 .js with "type": "module")
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// 기본 내보내기
export default function multiply(a, b) {
return a * b;
}
가져오기 (import):
// app.mjs
// 필요한 모듈 import
import multiply, { add, subtract, PI } from './math.mjs';
console.log(add(10, 5)); // 15
console.log(multiply(10, 5)); // 50
console.log(PI); // 3.14159
// 모두 가져오기
import * as math from './math.mjs';
console.log(math.add(10, 5));
내장 모듈
// 파일 시스템
const fs = require('fs');
// 경로 처리
const path = require('path');
// HTTP
const http = require('http');
// URL 처리
const url = require('url');
// 운영체제 정보
const os = require('os');
// 이벤트
const EventEmitter = require('events');
7. 파일 시스템 (fs)
동기 vs 비동기
Node.js의 핵심인 동기·비동기 처리 방식을 짚어 봅니다. 동기 (Sync): 작업이 끝날 때까지 대기
const fs = require('fs');
try {
// readFileSync: 파일을 다 읽을 때까지 다음 코드 실행 안 됨 (블로킹)
// 파일이 크면 프로그램이 멈춘 것처럼 보임
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data); // 파일 내용 출력
} catch (err) {
// 파일이 없거나 권한이 없으면 에러 발생
console.error('에러:', err.message);
}
// 파일 읽기가 완전히 끝난 후에 실행됨
console.log('파일 읽기 완료');
동기 방식의 문제점:
- 파일 읽기가 끝날 때까지 다른 작업을 할 수 없음
- 서버에서 사용하면 다른 요청을 처리할 수 없어 성능 저하
- 초기화 코드나 간단한 스크립트에만 사용 권장 비동기 (Callback): 작업을 백그라운드에서 실행
const fs = require('fs');
// readFile: 파일 읽기를 백그라운드에서 실행 (논블로킹)
// 파일 읽기가 완료되면 콜백 함수가 호출됨
fs.readFile('file.txt', 'utf8', (err, data) => {
// 콜백 함수: 작업 완료 후 실행되는 함수
// Node.js 관례: 첫 번째 인자는 항상 에러 객체
if (err) {
console.error('에러:', err.message);
return; // 에러가 있으면 여기서 종료
}
// 에러가 없으면 data에 파일 내용이 담김
console.log(data);
});
// 파일 읽기와 동시에 바로 실행됨 (대기하지 않음)
console.log('파일 읽기 시작됨');
// 출력 순서: "파일 읽기 시작됨" → 파일 내용
비동기 방식의 장점:
- 파일 읽기 중에도 다른 작업 가능
- 서버가 여러 요청을 동시에 처리 가능
- Node.js의 핵심 강점 비동기 (Promise): 최신 방식
// fs.promises: Promise 기반 파일 시스템 API
const fs = require('fs').promises;
async function readFileAsync() {
try {
// await: Promise가 완료될 때까지 대기 (하지만 다른 작업은 블로킹하지 않음)
// 코드는 동기처럼 보이지만 실제로는 비동기로 동작
const data = await fs.readFile('file.txt', 'utf8');
console.log(data);
} catch (err) {
// try-catch로 에러 처리 (콜백보다 직관적)
console.error('에러:', err.message);
}
}
// async 함수는 항상 Promise를 반환
readFileAsync();
Promise 방식의 장점:
- 콜백 지옥(callback hell) 방지
- 에러 처리가 더 직관적 (try-catch)
- 코드 가독성 향상
- 현대적인 JavaScript 스타일
파일 읽기/쓰기
const fs = require('fs').promises;
async function fileOperations() {
try {
// 파일 쓰기
await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
console.log('파일 쓰기 완료');
// 파일 읽기
const data = await fs.readFile('output.txt', 'utf8');
console.log('파일 내용:', data);
// 파일 추가
await fs.appendFile('output.txt', '\n추가 내용', 'utf8');
// 파일 존재 확인
const exists = await fs.access('output.txt')
.then(() => true)
.catch(() => false);
console.log('파일 존재:', exists);
// 파일 삭제
await fs.unlink('output.txt');
console.log('파일 삭제 완료');
} catch (err) {
console.error('에러:', err.message);
}
}
fileOperations();
8. 실전 예제
예제 1: 간단한 웹 서버
// simple-server.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const server = http.createServer(async (req, res) => {
console.log(`${req.method} ${req.url}`);
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<html>
<head><title>Node.js 서버</title></head>
<body>
<h1>환영합니다!</h1>
<ul>
<li><a href="/about">소개</a></li>
<li><a href="/api/time">현재 시간 API</a></li>
</ul>
</body>
</html>
`);
} else if (req.url === '/about') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>소개 페이지</h1><p>Node.js로 만든 서버입니다.</p>');
} else if (req.url === '/api/time') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
time: new Date().toISOString(),
timestamp: Date.now()
}));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('404 - 페이지를 찾을 수 없습니다');
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`서버 실행 중: http://localhost:${PORT}`);
console.log('종료하려면 Ctrl+C를 누르세요');
});
예제 2: 파일 서버
// file-server.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.txt': 'text/plain'
};
const server = http.createServer(async (req, res) => {
try {
let filePath = '.' + req.url;
if (filePath === './') {
filePath = './index.html';
}
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
const data = await fs.readFile(filePath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
console.log(`✓ ${req.url}`);
} catch (err) {
if (err.code === 'ENOENT') {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 - File Not Found');
} else {
res.writeHead(500);
res.end('500 - Internal Server Error');
}
console.error(`✗ ${req.url}: ${err.message}`);
}
});
server.listen(3000, () => {
console.log('파일 서버 실행 중: http://localhost:3000');
});
예제 3: CLI 도구
// cli-tool.js
const fs = require('fs').promises;
const path = require('path');
async function countFiles(directory) {
try {
const files = await fs.readdir(directory);
let fileCount = 0;
let dirCount = 0;
for (const file of files) {
const filePath = path.join(directory, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
dirCount++;
} else {
fileCount++;
}
}
console.log(`\n디렉토리: ${directory}`);
console.log(`파일: ${fileCount}개`);
console.log(`폴더: ${dirCount}개`);
console.log(`총: ${fileCount + dirCount}개`);
} catch (err) {
console.error('에러:', err.message);
process.exit(1);
}
}
// 명령줄 인자로 디렉토리 받기
const directory = process.argv[2] || '.';
countFiles(directory);
실행:
node cli-tool.js
node cli-tool.js ./src
9. nodemon (자동 재시작)
설치
# 전역 설치
npm install -g nodemon
# 프로젝트 개발 의존성으로 설치
npm install --save-dev nodemon
사용
# node 대신 nodemon 사용
nodemon server.js
# 파일 변경 시 자동으로 재시작됨
package.json 설정
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
npm run dev
nodemon.json 설정
{
"watch": [src],
"ext": "js,json",
"ignore": ["node_modules", "test"],
"delay": 1000
}
10. 디버깅
console.log 디버깅
console.log('변수:', variable);
console.log('객체:', JSON.stringify(obj, null, 2));
console.error('에러:', error);
console.warn('경고:', warning);
console.table([{ name: '홍길동', age: 25 }]);
VS Code 디버거
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [<node_internals>/**],
"program": "${workspaceFolder}/index.js"
}
]
}
사용법:
- 중단점(breakpoint) 설정
F5또는 디버그 시작- 변수 값 확인, 단계별 실행
Node.js 내장 디버거
node inspect server.js
# 디버거 명령어
# cont (c): 계속 실행
# next (n): 다음 줄
# step (s): 함수 안으로
# out (o): 함수 밖으로
# repl: REPL 모드
11. 환경 변수 관리
.env 파일
# .env 파일 설치
npm install dotenv
.env:
PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/mydb
API_KEY=your-secret-key
사용:
// config.js
require('dotenv').config();
const config = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
databaseUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY
};
module.exports = config;
// server.js
const config = require('./config');
console.log(`포트: ${config.port}`);
console.log(`환경: ${config.nodeEnv}`);
.gitignore:
node_modules/
.env
12. 자주 발생하는 문제
문제 1: 포트 이미 사용 중
에러:
Error: listen EADDRINUSE: address already in use :::3000
해결:
# Windows
netstat -ano | findstr :3000
taskkill /PID <PID> /F
# Linux/macOS
lsof -i :3000
kill -9 <PID>
# 또는 다른 포트 사용
const PORT = process.env.PORT || 3001;
문제 2: 모듈을 찾을 수 없음
에러:
Error: Cannot find module 'express'
해결:
# node_modules 확인
ls node_modules
# 패키지 재설치
npm install
# 특정 패키지 설치
npm install express
문제 3: 경로 문제
// ❌ 잘못된 경로
const data = fs.readFileSync('file.txt');
// ✅ 절대 경로 사용
const path = require('path');
const filePath = path.join(__dirname, 'file.txt');
const data = fs.readFileSync(filePath, 'utf8');
문제 4: 비동기 처리 실수
// ❌ 비동기 결과를 기다리지 않음
fs.readFile('file.txt', 'utf8', (err, data) => {
console.log(data);
});
console.log('완료'); // 먼저 출력됨!
// ✅ async/await 사용
async function readFile() {
const data = await fs.promises.readFile('file.txt', 'utf8');
console.log(data);
console.log('완료');
}
13. 실전 팁
프로젝트 구조
my-node-project/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ └── utils/
├── public/
├── tests/
├── .env
├── .gitignore
├── package.json
├── README.md
└── server.js
베스트 프랙티스
// ✅ 에러 처리
process.on('uncaughtException', (err) => {
console.error('예상치 못한 에러:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('처리되지 않은 Promise 거부:', reason);
});
// ✅ Graceful Shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM 신호 받음. 서버 종료 중...');
server.close(() => {
console.log('서버 종료됨');
process.exit(0);
});
});
// ✅ 환경 변수 사용
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
// ✅ 로깅
const isDev = NODE_ENV === 'development';
if (isDev) {
console.log('개발 모드');
}
성능 최적화
// ✅ 스트림 사용 (대용량 파일)
// 변수 선언 및 초기화
const fs = require('fs');
const readStream = fs.createReadStream('large-file.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.pipe(writeStream);
// ✅ 클러스터링 (멀티 코어 활용)
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
// 워커 프로세스에서 서버 실행
http.createServer((req, res) => {
res.end('Hello');
}).listen(3000);
}
정리
핵심 요약
- Node.js: Chrome V8 기반 JavaScript 런타임 — V8은 실행·GC, libuv는 이벤트 루프·I/O·스레드 풀
- 설치: nodejs.org에서 LTS 버전 다운로드
- 실행:
node filename.js - npm: 패키지 관리자,
npm install <package> - 모듈: CommonJS (
require) 또는 ES Modules (import) - 파일 시스템:
fs모듈, 동기/비동기 - HTTP 서버:
http.createServer() - 개발 도구: nodemon, VS Code 디버거
- 호출 경로: JS → C++ 바인딩 → libuv/OS → 완료 시 콜백/Promise 후속이 이벤트 루프에서 실행
Node.js 장점
- JavaScript 통일: 프론트엔드와 백엔드 모두 JavaScript
- 비동기 I/O: 높은 동시 처리 성능
- npm 생태계: 수백만 개의 패키지
- 빠른 개발: 간결한 코드, 빠른 프로토타이핑
- 활발한 커뮤니티: 풍부한 학습 자료
다음 단계
추천 학습 자료
공식 문서:
- Node.js 공식 문서
- npm 공식 문서 튜토리얼:
- Node.js 공식 가이드
- freeCodeCamp Node.js 책:
- “Node.js 디자인 패턴”
- “Node.js 교과서”
다른 언어와 비교
관련 글
- Node.js 시리즈 전체 보기
- Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
- Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await
- Express.js 완벽 가이드 | Node.js 웹 프레임워크
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Node.js 시작하기 | 설치, 설정, Hello World」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Node.js 시작하기 | 설치, 설정, Hello World」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Node.js 시작하기: 설치, 설정, Hello World. Node.js vs 브라우저 JavaScript·Node.js 설치로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
- JavaScript 비동기 프로그래밍 | Promise, async/await 완벽 정리
- [2026] Node.js 백엔드 완전 정리 — libuv·워커·스트림·우아한 종료·프로덕션
이 글에서 다루는 키워드 (관련 검색어)
Node.js, JavaScript, 백엔드, 서버, npm, V8, libuv, 입문 등으로 검색하시면 이 글이 도움이 됩니다.