Node.js 시작하기 | 설치, 설정, Hello World

Node.js 시작하기 | 설치, 설정, Hello World

이 글의 핵심

Node.js 시작하기에 대한 실전 가이드입니다. 설치, 설정, Hello World 등을 예제와 함께 상세히 설명합니다.

들어가며

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 집약적 작업 (이미지/비디오 처리)
  • ❌ 복잡한 계산 작업

1. Node.js vs 브라우저 JavaScript

비교

특징브라우저 JavaScriptNode.js
실행 환경브라우저 (Chrome, Firefox)서버, 로컬 머신
전역 객체windowglobal
DOM 접근
파일 시스템✅ (fs 모듈)
HTTP 서버✅ (http 모듈)
모듈 시스템ES ModulesCommonJS + 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 문법으로 Promiseasync/await를 씁니다. JSON.parseJSON.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

  1. nodejs.org 방문
  2. LTS 버전 다운로드 (안정 버전, 권장)
  3. 설치 프로그램 실행
  4. 설치 확인:
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} 에서 실행 중`);
});

코드 흐름:

  1. http 모듈을 불러옵니다.
  2. createServer요청이 올 때마다 호출될 함수를 넘깁니다(콜백).
  3. listen으로 포트 3000에서 연결을 받을 준비를 합니다.
  4. 브라우저 등이 접속하면 req·res가 채워진 채로 위 콜백이 실행됩니다.
  5. 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');
});

라우팅 동작 원리:

  1. 클라이언트가 특정 URL로 요청
  2. req.urlreq.method를 확인
  3. 조건문으로 매칭되는 경로 찾기
  4. 해당 경로에 맞는 응답 반환
  5. 매칭되는 경로가 없으면 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 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"
    }
  ]
}

사용법:

  1. 중단점(breakpoint) 설정
  2. F5 또는 디버그 시작
  3. 변수 값 확인, 단계별 실행

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

정리

핵심 요약

  1. Node.js: Chrome V8 기반 JavaScript 런타임
  2. 설치: nodejs.org에서 LTS 버전 다운로드
  3. 실행: node filename.js
  4. npm: 패키지 관리자, npm install <package>
  5. 모듈: CommonJS (require) 또는 ES Modules (import)
  6. 파일 시스템: fs 모듈, 동기/비동기
  7. HTTP 서버: http.createServer()
  8. 개발 도구: nodemon, VS Code 디버거

Node.js 장점

  • JavaScript 통일: 프론트엔드와 백엔드 모두 JavaScript
  • 비동기 I/O: 높은 동시 처리 성능
  • npm 생태계: 수백만 개의 패키지
  • 빠른 개발: 간결한 코드, 빠른 프로토타이핑
  • 활발한 커뮤니티: 풍부한 학습 자료

다음 단계

  • Node.js 모듈 시스템
  • Node.js 비동기 프로그래밍
  • Express.js 웹 프레임워크

추천 학습 자료

공식 문서:

튜토리얼:

:

  • “Node.js 디자인 패턴”
  • “Node.js 교과서”

다른 언어와 비교

  • JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문

관련 글

  • Node.js 시리즈 전체 보기
  • Node.js 모듈 시스템 | CommonJS와 ES Modules 완벽 가이드
  • Node.js 비동기 프로그래밍 | Callback, Promise, Async/Await
  • Express.js 완벽 가이드 | Node.js 웹 프레임워크