<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>걷고 또 걷기</title>
    <link>https://walking-and-walking.tistory.com/</link>
    <description>운동을 좋아하는 8년차 웹 개발자 입니다. </description>
    <language>ko</language>
    <pubDate>Fri, 29 May 2026 03:26:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>wsstar</managingEditor>
    <image>
      <title>걷고 또 걷기</title>
      <url>https://tistory1.daumcdn.net/tistory/3203295/attach/19c4d40f167b4f12ae089f2d2f3a78e8</url>
      <link>https://walking-and-walking.tistory.com</link>
    </image>
    <item>
      <title>Node.js Redis 연동 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-Redis-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션에서 Redis를 연동하면 캐싱, 세션 관리, 실시간 메시징 등 다양한 기능을 구현할 수 있습니다. 이 글에서는 ioredis 패키지를 사용하여 Redis를 효과적으로 활용하는 방법을 알아봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Redis 소개와 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis(Remote Dictionary Server)는 오픈소스 인메모리 데이터 저장소입니다. 모든 데이터를 메모리에 저장하기 때문에 읽기와 쓰기 속도가 매우 빠릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis의 주요 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인메모리 저장: 데이터를 RAM에 저장하여 마이크로초 단위의 응답 시간 제공&lt;/li&gt;
&lt;li&gt;다양한 데이터 구조: String, Hash, List, Set, Sorted Set, Stream 등 지원&lt;/li&gt;
&lt;li&gt;영속성: RDB 스냅샷과 AOF 로그를 통한 데이터 영구 저장 옵션&lt;/li&gt;
&lt;li&gt;복제: 마스터-슬레이브 복제를 통한 고가용성 구현&lt;/li&gt;
&lt;li&gt;Pub/Sub: 메시지 발행/구독 패턴 지원&lt;/li&gt;
&lt;li&gt;클러스터: 수평 확장을 위한 클러스터 모드 지원&lt;/li&gt;
&lt;li&gt;트랜잭션: MULTI/EXEC 명령어를 통한 원자적 연산&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ioredis 패키지 설치 및 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 Redis를 사용하기 위해 ioredis 패키지를 사용합니다. ioredis는 기능이 풍부하고 성능이 우수한 Redis 클라이언트입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install ioredis&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript를 사용하는 경우 타입 정의가 패키지에 포함되어 있어 별도 설치가 필요 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 기본 연결 설정&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Redis = require('ioredis');

// 기본 연결 (localhost:6379)
const redis = new Redis();

// 호스트와 포트 지정
const redis = new Redis(6379, '127.0.0.1');

// URL 형식으로 연결
const redis = new Redis('redis://username:password@host:6379/0');

// 옵션 객체로 연결
const redis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  password: 'your-password',
  db: 0,
  retryStrategy: (times) =&amp;gt; {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  connectTimeout: 10000
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 연결 이벤트 처리&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const Redis = require('ioredis');
const redis = new Redis();

redis.on('connect', () =&amp;gt; {
  console.log('Redis 연결 시작');
});

redis.on('ready', () =&amp;gt; {
  console.log('Redis 연결 준비 완료');
});

redis.on('error', (err) =&amp;gt; {
  console.error('Redis 에러:', err);
});

redis.on('close', () =&amp;gt; {
  console.log('Redis 연결 종료');
});

redis.on('reconnecting', () =&amp;gt; {
  console.log('Redis 재연결 시도 중');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기본 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 SET과 GET&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const Redis = require('ioredis');
const redis = new Redis();

async function basicOperations() {
  // 값 저장
  await redis.set('name', 'John');

  // 값 조회
  const name = await redis.get('name');
  console.log(name); // 'John'

  // 존재하지 않는 키 조회
  const notExist = await redis.get('notExist');
  console.log(notExist); // null

  // NX 옵션: 키가 없을 때만 저장
  await redis.set('name', 'Jane', 'NX'); // 실패 (이미 존재)

  // XX 옵션: 키가 있을 때만 저장
  await redis.set('name', 'Jane', 'XX'); // 성공

  // EX 옵션: 초 단위 만료 시간 설정
  await redis.set('session', 'abc123', 'EX', 3600);

  // PX 옵션: 밀리초 단위 만료 시간 설정
  await redis.set('temp', 'data', 'PX', 5000);
}

basicOperations();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 DEL과 EXISTS&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function deleteAndCheck() {
  await redis.set('key1', 'value1');
  await redis.set('key2', 'value2');
  await redis.set('key3', 'value3');

  // 키 삭제 (단일)
  const deleted = await redis.del('key1');
  console.log(deleted); // 1 (삭제된 키 개수)

  // 키 삭제 (다중)
  const deletedMultiple = await redis.del('key2', 'key3');
  console.log(deletedMultiple); // 2

  // 키 존재 확인
  const exists = await redis.exists('key1');
  console.log(exists); // 0 (존재하지 않음)

  await redis.set('active', 'true');
  const activeExists = await redis.exists('active');
  console.log(activeExists); // 1 (존재함)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 EXPIRE와 TTL&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function expireOperations() {
  await redis.set('token', 'xyz789');

  // 만료 시간 설정 (초 단위)
  await redis.expire('token', 3600);

  // 남은 시간 확인 (초 단위)
  const ttl = await redis.ttl('token');
  console.log(ttl); // 3600 (또는 그보다 작은 값)

  // 남은 시간 확인 (밀리초 단위)
  const pttl = await redis.pttl('token');
  console.log(pttl); // 밀리초 단위 값

  // 특정 시점에 만료 (Unix timestamp)
  const expireAt = Math.floor(Date.now() / 1000) + 7200;
  await redis.expireat('token', expireAt);

  // 만료 시간 제거 (영구 저장)
  await redis.persist('token');
  const afterPersist = await redis.ttl('token');
  console.log(afterPersist); // -1 (만료 시간 없음)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 INCR과 DECR&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function counterOperations() {
  // 카운터 초기화
  await redis.set('visitors', '0');

  // 1씩 증가
  const incr = await redis.incr('visitors');
  console.log(incr); // 1

  // 지정 값만큼 증가
  const incrby = await redis.incrby('visitors', 10);
  console.log(incrby); // 11

  // 1씩 감소
  const decr = await redis.decr('visitors');
  console.log(decr); // 10

  // 지정 값만큼 감소
  const decrby = await redis.decrby('visitors', 5);
  console.log(decrby); // 5

  // 부동소수점 증가
  await redis.set('price', '10.5');
  const incrbyfloat = await redis.incrbyfloat('price', 0.5);
  console.log(incrbyfloat); // '11'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 데이터 구조 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Hash&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash는 필드-값 쌍의 집합으로, 객체를 저장하기에 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function hashOperations() {
  // 단일 필드 설정
  await redis.hset('user:1001', 'name', 'John');
  await redis.hset('user:1001', 'email', 'john@example.com');

  // 다중 필드 설정
  await redis.hset('user:1002', {
    name: 'Jane',
    email: 'jane@example.com',
    age: '30'
  });

  // 단일 필드 조회
  const name = await redis.hget('user:1001', 'name');
  console.log(name); // 'John'

  // 다중 필드 조회
  const fields = await redis.hmget('user:1002', 'name', 'email');
  console.log(fields); // ['Jane', 'jane@example.com']

  // 모든 필드-값 조회
  const user = await redis.hgetall('user:1002');
  console.log(user); // { name: 'Jane', email: 'jane@example.com', age: '30' }

  // 필드 존재 확인
  const exists = await redis.hexists('user:1001', 'name');
  console.log(exists); // 1

  // 필드 삭제
  await redis.hdel('user:1001', 'email');

  // 모든 필드명 조회
  const keys = await redis.hkeys('user:1002');
  console.log(keys); // ['name', 'email', 'age']

  // 모든 값 조회
  const values = await redis.hvals('user:1002');
  console.log(values); // ['Jane', 'jane@example.com', '30']

  // 필드 개수
  const len = await redis.hlen('user:1002');
  console.log(len); // 3

  // 숫자 필드 증가
  await redis.hincrby('user:1002', 'age', 1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 List&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List는 순서가 있는 문자열 집합으로, 큐나 스택 구현에 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function listOperations() {
  // 왼쪽에 추가 (스택처럼 사용)
  await redis.lpush('tasks', 'task1', 'task2', 'task3');
  // 결과: ['task3', 'task2', 'task1']

  // 오른쪽에 추가 (큐처럼 사용)
  await redis.rpush('tasks', 'task4');
  // 결과: ['task3', 'task2', 'task1', 'task4']

  // 범위 조회 (0부터 시작, -1은 마지막)
  const allTasks = await redis.lrange('tasks', 0, -1);
  console.log(allTasks); // ['task3', 'task2', 'task1', 'task4']

  // 인덱스로 조회
  const first = await redis.lindex('tasks', 0);
  console.log(first); // 'task3'

  // 왼쪽에서 꺼내기
  const leftPop = await redis.lpop('tasks');
  console.log(leftPop); // 'task3'

  // 오른쪽에서 꺼내기
  const rightPop = await redis.rpop('tasks');
  console.log(rightPop); // 'task4'

  // 리스트 길이
  const len = await redis.llen('tasks');
  console.log(len); // 2

  // 인덱스로 값 변경
  await redis.lset('tasks', 0, 'updated-task');

  // 특정 범위만 유지
  await redis.ltrim('tasks', 0, 99); // 처음 100개만 유지

  // 블로킹 팝 (큐 대기)
  // 5초 동안 대기하며 값이 오면 꺼냄
  const blocked = await redis.blpop('queue', 5);
  console.log(blocked); // ['queue', 'value'] 또는 null
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 Set&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set은 중복 없는 문자열 집합입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function setOperations() {
  // 멤버 추가
  await redis.sadd('tags', 'nodejs', 'javascript', 'redis');
  await redis.sadd('tags', 'nodejs'); // 중복은 무시됨

  // 모든 멤버 조회
  const members = await redis.smembers('tags');
  console.log(members); // ['nodejs', 'javascript', 'redis']

  // 멤버 존재 확인
  const isMember = await redis.sismember('tags', 'nodejs');
  console.log(isMember); // 1

  // 멤버 개수
  const count = await redis.scard('tags');
  console.log(count); // 3

  // 멤버 삭제
  await redis.srem('tags', 'redis');

  // 랜덤 멤버 조회
  const random = await redis.srandmember('tags');
  console.log(random); // 랜덤 값

  // 랜덤 멤버 꺼내기
  const popped = await redis.spop('tags');
  console.log(popped); // 랜덤 값 (집합에서 제거됨)

  // 집합 연산
  await redis.sadd('set1', 'a', 'b', 'c');
  await redis.sadd('set2', 'b', 'c', 'd');

  // 합집합
  const union = await redis.sunion('set1', 'set2');
  console.log(union); // ['a', 'b', 'c', 'd']

  // 교집합
  const inter = await redis.sinter('set1', 'set2');
  console.log(inter); // ['b', 'c']

  // 차집합
  const diff = await redis.sdiff('set1', 'set2');
  console.log(diff); // ['a']
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 Sorted Set&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set은 점수를 기준으로 정렬되는 집합입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function sortedSetOperations() {
  // 멤버 추가 (점수, 멤버)
  await redis.zadd('leaderboard', 100, 'player1');
  await redis.zadd('leaderboard', 200, 'player2');
  await redis.zadd('leaderboard', 150, 'player3');

  // 다중 추가
  await redis.zadd('leaderboard', 180, 'player4', 120, 'player5');

  // 점수순 조회 (오름차순)
  const ascending = await redis.zrange('leaderboard', 0, -1);
  console.log(ascending); // ['player1', 'player5', 'player3', 'player4', 'player2']

  // 점수순 조회 (내림차순)
  const descending = await redis.zrevrange('leaderboard', 0, -1);
  console.log(descending); // ['player2', 'player4', 'player3', 'player5', 'player1']

  // 점수와 함께 조회
  const withScores = await redis.zrange('leaderboard', 0, -1, 'WITHSCORES');
  console.log(withScores); // ['player1', '100', 'player5', '120', ...]

  // 특정 멤버 점수 조회
  const score = await redis.zscore('leaderboard', 'player1');
  console.log(score); // '100'

  // 순위 조회 (0부터 시작)
  const rank = await redis.zrank('leaderboard', 'player1');
  console.log(rank); // 0 (오름차순 기준)

  const revRank = await redis.zrevrank('leaderboard', 'player1');
  console.log(revRank); // 4 (내림차순 기준)

  // 점수 증가
  await redis.zincrby('leaderboard', 50, 'player1');

  // 점수 범위로 조회
  const byScore = await redis.zrangebyscore('leaderboard', 100, 200);
  console.log(byScore);

  // 멤버 삭제
  await redis.zrem('leaderboard', 'player5');

  // 멤버 개수
  const count = await redis.zcard('leaderboard');
  console.log(count);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Pub/Sub 패턴 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub은 실시간 메시징을 위한 패턴입니다. 발행자가 메시지를 발행하면 구독자가 이를 받습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 기본 Pub/Sub&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Redis = require('ioredis');

// 구독자 (별도 연결 필요)
const subscriber = new Redis();

// 발행자
const publisher = new Redis();

// 채널 구독
subscriber.subscribe('news', 'updates', (err, count) =&amp;gt; {
  if (err) {
    console.error('구독 실패:', err);
    return;
  }
  console.log(`${count}개 채널 구독 중`);
});

// 메시지 수신
subscriber.on('message', (channel, message) =&amp;gt; {
  console.log(`[${channel}] ${message}`);
});

// 메시지 발행
async function publish() {
  await publisher.publish('news', '새로운 소식입니다');
  await publisher.publish('updates', '업데이트가 있습니다');
}

publish();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 패턴 구독&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Redis = require('ioredis');
const subscriber = new Redis();
const publisher = new Redis();

// 패턴으로 구독 (와일드카드 사용)
subscriber.psubscribe('chat:*', (err, count) =&amp;gt; {
  console.log(`패턴 구독 완료: ${count}`);
});

// 패턴 매칭 메시지 수신
subscriber.on('pmessage', (pattern, channel, message) =&amp;gt; {
  console.log(`패턴: ${pattern}, 채널: ${channel}, 메시지: ${message}`);
});

// 다양한 채널에 발행
async function publishToRooms() {
  await publisher.publish('chat:room1', '안녕하세요');
  await publisher.publish('chat:room2', '반갑습니다');
  await publisher.publish('chat:lobby', '로비 메시지');
}

publishToRooms();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 실시간 알림 시스템 예제&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const Redis = require('ioredis');

class NotificationService {
  constructor() {
    this.subscriber = new Redis();
    this.publisher = new Redis();
    this.handlers = new Map();
  }

  subscribe(channel, handler) {
    if (!this.handlers.has(channel)) {
      this.handlers.set(channel, []);
      this.subscriber.subscribe(channel);
    }
    this.handlers.get(channel).push(handler);
  }

  init() {
    this.subscriber.on('message', (channel, message) =&amp;gt; {
      const handlers = this.handlers.get(channel) || [];
      const data = JSON.parse(message);
      handlers.forEach(handler =&amp;gt; handler(data));
    });
  }

  async notify(channel, data) {
    await this.publisher.publish(channel, JSON.stringify(data));
  }

  async close() {
    await this.subscriber.quit();
    await this.publisher.quit();
  }
}

// 사용 예시
const notifications = new NotificationService();
notifications.init();

notifications.subscribe('orders', (data) =&amp;gt; {
  console.log('새 주문:', data);
});

notifications.notify('orders', {
  orderId: '12345',
  product: 'Node.js 책',
  quantity: 1
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 파이프라인과 트랜잭션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프라인은 여러 명령을 한 번에 전송하여 네트워크 왕복을 줄입니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const Redis = require('ioredis');
const redis = new Redis();

async function pipelineExample() {
  // 파이프라인 생성
  const pipeline = redis.pipeline();

  // 명령 추가
  pipeline.set('key1', 'value1');
  pipeline.set('key2', 'value2');
  pipeline.get('key1');
  pipeline.get('key2');
  pipeline.incr('counter');

  // 실행 및 결과 받기
  const results = await pipeline.exec();
  console.log(results);
  // [
  //   [null, 'OK'],
  //   [null, 'OK'],
  //   [null, 'value1'],
  //   [null, 'value2'],
  //   [null, 1]
  // ]

  // 체이닝 방식
  const chainResults = await redis.pipeline()
    .set('a', '1')
    .set('b', '2')
    .get('a')
    .get('b')
    .exec();

  console.log(chainResults);
}

pipelineExample();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 트랜잭션 (MULTI/EXEC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 여러 명령을 원자적으로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;async function transactionExample() {
  // 기본 트랜잭션
  const results = await redis.multi()
    .set('foo', 'bar')
    .get('foo')
    .incr('counter')
    .exec();

  console.log(results);
  // [
  //   [null, 'OK'],
  //   [null, 'bar'],
  //   [null, 1]
  // ]

  // 트랜잭션 취소
  const multi = redis.multi();
  multi.set('key', 'value');
  multi.discard(); // 트랜잭션 취소
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 WATCH를 사용한 낙관적 잠금&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function watchExample() {
  // 계좌 이체 예제
  async function transfer(from, to, amount) {
    while (true) {
      try {
        // 키 감시 시작
        await redis.watch(from, to);

        const fromBalance = parseInt(await redis.get(from)) || 0;
        const toBalance = parseInt(await redis.get(to)) || 0;

        if (fromBalance &amp;lt; amount) {
          await redis.unwatch();
          throw new Error('잔액 부족');
        }

        // 트랜잭션 실행
        const result = await redis.multi()
          .set(from, fromBalance - amount)
          .set(to, toBalance + amount)
          .exec();

        if (result === null) {
          // 다른 클라이언트가 값을 변경함, 재시도
          console.log('충돌 발생, 재시도...');
          continue;
        }

        console.log('이체 성공');
        return true;
      } catch (error) {
        await redis.unwatch();
        throw error;
      }
    }
  }

  // 초기 잔액 설정
  await redis.set('account:A', 1000);
  await redis.set('account:B', 500);

  // 이체 실행
  await transfer('account:A', 'account:B', 100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 연결 관리 및 에러 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 연결 풀 설정&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;const Redis = require('ioredis');

// 클러스터 모드
const cluster = new Redis.Cluster([
  { host: '127.0.0.1', port: 7000 },
  { host: '127.0.0.1', port: 7001 },
  { host: '127.0.0.1', port: 7002 }
], {
  redisOptions: {
    password: 'password'
  },
  scaleReads: 'slave' // 읽기는 슬레이브에서
});

// 센티넬 모드
const sentinel = new Redis({
  sentinels: [
    { host: '127.0.0.1', port: 26379 },
    { host: '127.0.0.1', port: 26380 }
  ],
  name: 'mymaster',
  password: 'password'
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 재연결 전략&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const redis = new Redis({
  retryStrategy: (times) =&amp;gt; {
    if (times &amp;gt; 10) {
      // 10번 이상 실패하면 재연결 중단
      return null;
    }
    // 재연결 대기 시간 (최대 3초)
    const delay = Math.min(times * 300, 3000);
    return delay;
  },
  reconnectOnError: (err) =&amp;gt; {
    // READONLY 에러 시 재연결
    const targetError = 'READONLY';
    if (err.message.includes(targetError)) {
      return true;
    }
    return false;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 에러 처리 패턴&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Redis = require('ioredis');

class RedisClient {
  constructor(options) {
    this.redis = new Redis({
      ...options,
      lazyConnect: true,
      maxRetriesPerRequest: 3
    });

    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.redis.on('error', (err) =&amp;gt; {
      console.error('Redis 에러:', err.message);
    });

    this.redis.on('connect', () =&amp;gt; {
      console.log('Redis 연결됨');
    });

    this.redis.on('ready', () =&amp;gt; {
      console.log('Redis 준비 완료');
    });

    this.redis.on('close', () =&amp;gt; {
      console.log('Redis 연결 종료');
    });
  }

  async connect() {
    try {
      await this.redis.connect();
    } catch (err) {
      console.error('연결 실패:', err.message);
      throw err;
    }
  }

  async get(key) {
    try {
      return await this.redis.get(key);
    } catch (err) {
      console.error(`GET 실패 [${key}]:`, err.message);
      return null;
    }
  }

  async set(key, value, ttl = null) {
    try {
      if (ttl) {
        return await this.redis.set(key, value, 'EX', ttl);
      }
      return await this.redis.set(key, value);
    } catch (err) {
      console.error(`SET 실패 [${key}]:`, err.message);
      return null;
    }
  }

  async del(key) {
    try {
      return await this.redis.del(key);
    } catch (err) {
      console.error(`DEL 실패 [${key}]:`, err.message);
      return 0;
    }
  }

  async disconnect() {
    await this.redis.quit();
  }
}

// 사용 예시
async function main() {
  const client = new RedisClient({
    host: '127.0.0.1',
    port: 6379
  });

  await client.connect();
  await client.set('greeting', 'Hello Redis', 3600);
  const value = await client.get('greeting');
  console.log(value);
  await client.disconnect();
}

main();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 타임아웃 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const redis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  connectTimeout: 10000, // 연결 타임아웃 10초
  commandTimeout: 5000,  // 명령 타임아웃 5초
  enableOfflineQueue: true, // 오프라인 시 명령 큐에 저장
  maxRetriesPerRequest: 3
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.5 정상 종료 처리&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;const Redis = require('ioredis');
const redis = new Redis();

async function gracefulShutdown() {
  console.log('종료 시작...');

  // 새로운 명령 거부
  redis.disconnect();

  // 대기 중인 명령 완료 후 종료
  await redis.quit();

  console.log('Redis 연결 정상 종료');
  process.exit(0);
}

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 Redis를 연동하면 빠른 캐싱, 세션 관리, 실시간 메시징 등 다양한 기능을 구현할 수 있습니다. ioredis는 풍부한 기능과 안정적인 연결 관리를 제공하여 프로덕션 환경에서도 신뢰할 수 있습니다. 파이프라인과 트랜잭션을 적절히 활용하고, 에러 처리와 재연결 전략을 잘 설정하면 안정적인 Redis 기반 애플리케이션을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/828</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-Redis-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry828comment</comments>
      <pubDate>Fri, 20 Mar 2026 08:00:52 +0900</pubDate>
    </item>
    <item>
      <title>Node.js SQLite 연동 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-SQLite-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 SQLite 데이터베이스를 연동하는 방법을 알아봅니다. 파일 기반의 경량 데이터베이스인 SQLite는 별도의 서버 설치 없이 사용할 수 있어 프로토타입 개발, 임베디드 애플리케이션, 소규모 프로젝트에 적합합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. SQLite 소개와 사용 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite는 서버리스, 설정이 필요 없는 자체 포함형 SQL 데이터베이스 엔진입니다. 데이터베이스 전체가 단일 파일로 저장되며, 크로스 플랫폼을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQLite 주요 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 프로세스 불필요 (Serverless)&lt;/li&gt;
&lt;li&gt;별도 설정 없이 바로 사용 가능 (Zero Configuration)&lt;/li&gt;
&lt;li&gt;단일 파일로 전체 데이터베이스 저장&lt;/li&gt;
&lt;li&gt;ACID 트랜잭션 지원&lt;/li&gt;
&lt;li&gt;최대 281TB 데이터베이스 크기 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적합한 사용 사례:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Electron 데스크톱 애플리케이션&lt;/li&gt;
&lt;li&gt;모바일 앱 로컬 저장소&lt;/li&gt;
&lt;li&gt;테스트 및 프로토타입 개발&lt;/li&gt;
&lt;li&gt;임베디드 시스템&lt;/li&gt;
&lt;li&gt;소규모 웹사이트 (동시 접속 10명 이하)&lt;/li&gt;
&lt;li&gt;데이터 분석 및 임시 저장소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부적합한 사용 사례:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고트래픽 웹 애플리케이션&lt;/li&gt;
&lt;li&gt;클라이언트/서버 구조가 필요한 경우&lt;/li&gt;
&lt;li&gt;대규모 동시 쓰기 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. better-sqlite3 vs sqlite3 패키지 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 SQLite를 사용할 때 주로 두 가지 패키지를 선택합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlite3 패키지&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install sqlite3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 API 기반 (콜백, Promise)&lt;/li&gt;
&lt;li&gt;Node.js 공식 sqlite3 바인딩&lt;/li&gt;
&lt;li&gt;가장 오래되고 널리 사용됨&lt;/li&gt;
&lt;li&gt;npm 주간 다운로드: 약 100만 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database('./mydb.sqlite', (err) =&amp;gt; {
  if (err) console.error(err.message);
  console.log('Connected to SQLite database.');
});

// 비동기 쿼리 (콜백 방식)
db.all('SELECT * FROM users', [], (err, rows) =&amp;gt; {
  if (err) throw err;
  console.log(rows);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3 패키지&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install better-sqlite3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기 API 기반&lt;/li&gt;
&lt;li&gt;더 빠른 성능 (2~5배)&lt;/li&gt;
&lt;li&gt;간결한 API&lt;/li&gt;
&lt;li&gt;트랜잭션 처리 용이&lt;/li&gt;
&lt;li&gt;Worker Threads와 함께 사용 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');

const db = new Database('./mydb.sqlite');

// 동기 쿼리
const rows = db.prepare('SELECT * FROM users').all();
console.log(rows);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;better-sqlite3&lt;/th&gt;
&lt;th&gt;sqlite3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API 방식&lt;/td&gt;
&lt;td&gt;동기&lt;/td&gt;
&lt;td&gt;비동기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단일 쿼리 성능&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대량 INSERT&lt;/td&gt;
&lt;td&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 사용량&lt;/td&gt;
&lt;td&gt;적음&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 복잡도&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택 기준:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 동기 작업, 성능 중시: &lt;b&gt;better-sqlite3&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기존 비동기 코드와 통합, async/await 선호: &lt;b&gt;sqlite3&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터베이스 연결 및 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3로 연결&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');

// 파일 기반 데이터베이스 생성/연결
const db = new Database('myapp.db');

// 옵션과 함께 연결
const dbWithOptions = new Database('myapp.db', {
  readonly: false,           // 읽기 전용 모드
  fileMustExist: false,      // 파일이 없으면 에러
  timeout: 5000,             // 잠금 대기 시간 (ms)
  verbose: console.log       // 쿼리 로깅
});

// 데이터베이스 닫기
db.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlite3로 연결&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const sqlite3 = require('sqlite3').verbose();

// 파일 기반 데이터베이스 생성/연결
const db = new sqlite3.Database('./myapp.db', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) =&amp;gt; {
  if (err) {
    console.error('Database connection error:', err.message);
  } else {
    console.log('Connected to SQLite database');
  }
});

// 데이터베이스 닫기
db.close((err) =&amp;gt; {
  if (err) console.error(err.message);
  console.log('Database connection closed');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 생성&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// better-sqlite3
const db = new Database('myapp.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,
    email TEXT NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    content TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
  )
`);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 동기/비동기 API 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3 (동기 API)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');
const db = new Database('sync-example.db');

// 단일 쿼리 실행
const info = db.prepare('INSERT INTO users (username, email) VALUES (?, ?)').run('john', 'john@example.com');
console.log('Inserted ID:', info.lastInsertRowid);
console.log('Changes:', info.changes);

// 단일 행 조회
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
console.log(user);

// 모든 행 조회
const allUsers = db.prepare('SELECT * FROM users').all();
console.log(allUsers);

// 이터레이터로 조회 (대량 데이터)
for (const row of db.prepare('SELECT * FROM users').iterate()) {
  console.log(row);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlite3 (비동기 API)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('./async-example.db');

// 콜백 방식
db.run('INSERT INTO users (username, email) VALUES (?, ?)', ['john', 'john@example.com'], function(err) {
  if (err) return console.error(err.message);
  console.log('Inserted ID:', this.lastID);
  console.log('Changes:', this.changes);
});

// 단일 행 조회
db.get('SELECT * FROM users WHERE id = ?', [1], (err, row) =&amp;gt; {
  if (err) return console.error(err.message);
  console.log(row);
});

// 모든 행 조회
db.all('SELECT * FROM users', [], (err, rows) =&amp;gt; {
  if (err) return console.error(err.message);
  console.log(rows);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlite3를 Promise로 래핑&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const sqlite3 = require('sqlite3').verbose();

class AsyncDatabase {
  constructor(filename) {
    this.db = new sqlite3.Database(filename);
  }

  run(sql, params = []) {
    return new Promise((resolve, reject) =&amp;gt; {
      this.db.run(sql, params, function(err) {
        if (err) reject(err);
        else resolve({ lastID: this.lastID, changes: this.changes });
      });
    });
  }

  get(sql, params = []) {
    return new Promise((resolve, reject) =&amp;gt; {
      this.db.get(sql, params, (err, row) =&amp;gt; {
        if (err) reject(err);
        else resolve(row);
      });
    });
  }

  all(sql, params = []) {
    return new Promise((resolve, reject) =&amp;gt; {
      this.db.all(sql, params, (err, rows) =&amp;gt; {
        if (err) reject(err);
        else resolve(rows);
      });
    });
  }

  close() {
    return new Promise((resolve, reject) =&amp;gt; {
      this.db.close((err) =&amp;gt; {
        if (err) reject(err);
        else resolve();
      });
    });
  }
}

// 사용 예시
async function main() {
  const db = new AsyncDatabase('./async-wrapped.db');

  await db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
  const result = await db.run('INSERT INTO users (name) VALUES (?)', ['Alice']);
  console.log('Inserted:', result.lastID);

  const users = await db.all('SELECT * FROM users');
  console.log(users);

  await db.close();
}

main().catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CRUD 작업 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3 CRUD&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');
const db = new Database('crud-example.db');

// 테이블 생성
db.exec(`
  CREATE TABLE IF NOT EXISTS products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    price REAL NOT NULL,
    stock INTEGER DEFAULT 0,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

// CREATE - 단일 삽입
const insertProduct = db.prepare('INSERT INTO products (name, price, stock) VALUES (?, ?, ?)');
const result = insertProduct.run('노트북', 1500000, 10);
console.log('생성된 ID:', result.lastInsertRowid);

// CREATE - 다중 삽입 (트랜잭션)
const insertMany = db.transaction((products) =&amp;gt; {
  for (const product of products) {
    insertProduct.run(product.name, product.price, product.stock);
  }
});

insertMany([
  { name: '마우스', price: 50000, stock: 100 },
  { name: '키보드', price: 80000, stock: 50 },
  { name: '모니터', price: 350000, stock: 20 }
]);

// READ - 단일 조회
const getProduct = db.prepare('SELECT * FROM products WHERE id = ?');
const product = getProduct.get(1);
console.log('단일 조회:', product);

// READ - 전체 조회
const getAllProducts = db.prepare('SELECT * FROM products ORDER BY id');
const allProducts = getAllProducts.all();
console.log('전체 조회:', allProducts);

// READ - 조건 조회
const getByPriceRange = db.prepare('SELECT * FROM products WHERE price BETWEEN ? AND ?');
const filtered = getByPriceRange.all(50000, 100000);
console.log('가격 필터:', filtered);

// UPDATE
const updateProduct = db.prepare('UPDATE products SET price = ?, stock = ? WHERE id = ?');
const updateResult = updateProduct.run(55000, 95, 2);
console.log('업데이트된 행:', updateResult.changes);

// DELETE
const deleteProduct = db.prepare('DELETE FROM products WHERE id = ?');
const deleteResult = deleteProduct.run(1);
console.log('삭제된 행:', deleteResult.changes);

db.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sqlite3 CRUD (async/await)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const sqlite3 = require('sqlite3').verbose();
const { promisify } = require('util');

const db = new sqlite3.Database('./crud-async.db');
const dbRun = promisify(db.run.bind(db));
const dbGet = promisify(db.get.bind(db));
const dbAll = promisify(db.all.bind(db));

async function crudExample() {
  // 테이블 생성
  await dbRun(`
    CREATE TABLE IF NOT EXISTS products (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      price REAL NOT NULL,
      stock INTEGER DEFAULT 0
    )
  `);

  // CREATE
  await dbRun('INSERT INTO products (name, price, stock) VALUES (?, ?, ?)', ['노트북', 1500000, 10]);

  // READ
  const product = await dbGet('SELECT * FROM products WHERE id = ?', [1]);
  console.log('조회:', product);

  const allProducts = await dbAll('SELECT * FROM products');
  console.log('전체:', allProducts);

  // UPDATE
  await dbRun('UPDATE products SET price = ? WHERE id = ?', [1400000, 1]);

  // DELETE
  await dbRun('DELETE FROM products WHERE id = ?', [1]);

  db.close();
}

crudExample().catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Prepared Statement 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prepared Statement는 SQL 인젝션 방지와 성능 향상에 필수적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3 Prepared Statement&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');
const db = new Database('prepared.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS employees (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    department TEXT,
    salary INTEGER
  )
`);

// Prepared Statement 생성
const insert = db.prepare('INSERT INTO employees (name, department, salary) VALUES (@name, @department, @salary)');
const selectByDept = db.prepare('SELECT * FROM employees WHERE department = ?');
const selectBySalary = db.prepare('SELECT * FROM employees WHERE salary &amp;gt; :minSalary AND salary &amp;lt; :maxSalary');

// 네임드 파라미터 사용
insert.run({ name: '김철수', department: '개발', salary: 5000000 });
insert.run({ name: '이영희', department: '디자인', salary: 4500000 });
insert.run({ name: '박지민', department: '개발', salary: 6000000 });

// 위치 파라미터
const developers = selectByDept.all('개발');
console.log('개발팀:', developers);

// 네임드 파라미터로 조회
const highPaid = selectBySalary.all({ minSalary: 4000000, maxSalary: 5500000 });
console.log('급여 범위:', highPaid);

// Statement 재사용으로 성능 향상
const insertEmployee = db.prepare('INSERT INTO employees (name, department, salary) VALUES (?, ?, ?)');

const employees = [
  ['홍길동', '마케팅', 4000000],
  ['정수연', '인사', 3800000],
  ['최민수', '개발', 5500000]
];

// 트랜잭션으로 대량 삽입
const insertAll = db.transaction((list) =&amp;gt; {
  for (const emp of list) {
    insertEmployee.run(...emp);
  }
  return list.length;
});

const count = insertAll(employees);
console.log(`${count}명 삽입 완료`);

// Prepared Statement 바인딩 해제 (필요시)
insert.bind({ name: '기본값', department: '미정', salary: 0 });

db.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 활용&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');
const db = new Database('transaction.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS accounts (
    id INTEGER PRIMARY KEY,
    name TEXT,
    balance INTEGER
  )
`);

// 초기 데이터
db.prepare('INSERT OR REPLACE INTO accounts (id, name, balance) VALUES (?, ?, ?)').run(1, 'Alice', 10000);
db.prepare('INSERT OR REPLACE INTO accounts (id, name, balance) VALUES (?, ?, ?)').run(2, 'Bob', 5000);

// 계좌 이체 트랜잭션
const transfer = db.transaction((fromId, toId, amount) =&amp;gt; {
  const from = db.prepare('SELECT balance FROM accounts WHERE id = ?').get(fromId);
  const to = db.prepare('SELECT balance FROM accounts WHERE id = ?').get(toId);

  if (!from || !to) {
    throw new Error('계좌를 찾을 수 없습니다');
  }

  if (from.balance &amp;lt; amount) {
    throw new Error('잔액이 부족합니다');
  }

  db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').run(amount, fromId);
  db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').run(amount, toId);

  return { from: fromId, to: toId, amount };
});

try {
  const result = transfer(1, 2, 3000);
  console.log('이체 완료:', result);
} catch (err) {
  console.error('이체 실패:', err.message);
}

// 결과 확인
const accounts = db.prepare('SELECT * FROM accounts').all();
console.log('계좌 현황:', accounts);

db.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 인메모리 데이터베이스 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인메모리 데이터베이스는 디스크 I/O 없이 메모리에서만 동작하여 매우 빠릅니다. 테스트, 캐싱, 임시 데이터 처리에 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;better-sqlite3 인메모리 데이터베이스&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');

// 인메모리 데이터베이스 생성
const memDb = new Database(':memory:');

// 테이블 생성 및 데이터 삽입
memDb.exec(`
  CREATE TABLE cache (
    key TEXT PRIMARY KEY,
    value TEXT,
    expires_at INTEGER
  )
`);

// 캐시 클래스 구현
class SQLiteCache {
  constructor() {
    this.db = new Database(':memory:');
    this.db.exec(`
      CREATE TABLE cache (
        key TEXT PRIMARY KEY,
        value TEXT,
        expires_at INTEGER
      )
    `);

    this.setStmt = this.db.prepare('INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)');
    this.getStmt = this.db.prepare('SELECT value, expires_at FROM cache WHERE key = ?');
    this.deleteStmt = this.db.prepare('DELETE FROM cache WHERE key = ?');
    this.cleanupStmt = this.db.prepare('DELETE FROM cache WHERE expires_at &amp;lt; ?');
  }

  set(key, value, ttlSeconds = 3600) {
    const expiresAt = Date.now() + (ttlSeconds * 1000);
    this.setStmt.run(key, JSON.stringify(value), expiresAt);
  }

  get(key) {
    const row = this.getStmt.get(key);
    if (!row) return null;
    if (row.expires_at &amp;lt; Date.now()) {
      this.deleteStmt.run(key);
      return null;
    }
    return JSON.parse(row.value);
  }

  delete(key) {
    this.deleteStmt.run(key);
  }

  cleanup() {
    return this.cleanupStmt.run(Date.now()).changes;
  }

  close() {
    this.db.close();
  }
}

// 사용 예시
const cache = new SQLiteCache();

cache.set('user:1', { name: 'John', email: 'john@example.com' }, 60);
cache.set('user:2', { name: 'Jane', email: 'jane@example.com' }, 120);

console.log('캐시 조회:', cache.get('user:1'));
console.log('없는 키:', cache.get('user:999'));

cache.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트용 인메모리 데이터베이스&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');

// 테스트 유틸리티
function createTestDatabase() {
  const db = new Database(':memory:');

  // 스키마 설정
  db.exec(`
    CREATE TABLE users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      username TEXT UNIQUE NOT NULL,
      email TEXT NOT NULL
    );

    CREATE TABLE posts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER,
      title TEXT NOT NULL,
      content TEXT,
      FOREIGN KEY (user_id) REFERENCES users(id)
    );
  `);

  return db;
}

// 테스트 예시 (Jest 스타일)
function testUserCreation() {
  const db = createTestDatabase();

  const insert = db.prepare('INSERT INTO users (username, email) VALUES (?, ?)');
  const result = insert.run('testuser', 'test@example.com');

  console.assert(result.lastInsertRowid === 1, 'ID should be 1');
  console.assert(result.changes === 1, 'Changes should be 1');

  const user = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
  console.assert(user.username === 'testuser', 'Username should match');

  db.close();
  console.log('테스트 통과');
}

testUserCreation();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일과 메모리 간 복사&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');

// 파일 데이터베이스를 메모리로 로드
function loadToMemory(filePath) {
  const fileDb = new Database(filePath, { readonly: true });
  const memDb = new Database(':memory:');

  // 백업 기능으로 복사
  fileDb.backup(memDb);
  fileDb.close();

  return memDb;
}

// 메모리 데이터베이스를 파일로 저장
function saveToFile(memDb, filePath) {
  const fileDb = new Database(filePath);
  memDb.backup(fileDb);
  fileDb.close();
}

// 사용 예시
const memDb = new Database(':memory:');
memDb.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)');
memDb.prepare('INSERT INTO test (value) VALUES (?)').run('Hello World');

saveToFile(memDb, './backup.db');
console.log('메모리 데이터베이스가 파일로 저장됨');

memDb.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 SQLite를 연동하면 별도의 데이터베이스 서버 없이 파일 기반으로 데이터를 관리할 수 있습니다. 성능과 간결한 동기 API가 필요하다면 better-sqlite3를, 기존 비동기 코드와의 통합이 중요하다면 sqlite3 패키지를 선택하세요. Prepared Statement와 트랜잭션을 적극 활용하면 안전하고 효율적인 데이터베이스 작업이 가능합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/827</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-SQLite-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry827comment</comments>
      <pubDate>Thu, 19 Mar 2026 20:00:23 +0900</pubDate>
    </item>
    <item>
      <title>Node.js PostgreSQL 연동</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-PostgreSQL-%EC%97%B0%EB%8F%99</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션에서 PostgreSQL 데이터베이스를 연동하는 방법을 알아봅니다. pg 패키지를 사용한 연결 설정부터 CRUD 작업, 트랜잭션, JSON 데이터 타입 활용까지 실무에서 바로 적용할 수 있는 내용을 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. PostgreSQL 소개와 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 오픈소스 객체-관계형 데이터베이스 관리 시스템(ORDBMS)입니다. 1986년 버클리 대학교의 POSTGRES 프로젝트에서 시작되어 현재까지 활발하게 개발되고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL의 주요 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACID 준수: 트랜잭션의 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 완벽하게 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 데이터 타입: 기본 타입 외에도 배열, hstore, JSON, JSONB, 기하학적 타입, 네트워크 주소 타입 등을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장성: 사용자 정의 함수, 데이터 타입, 연산자, 인덱스 메서드를 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 제어: MVCC(Multi-Version Concurrency Control)를 사용하여 읽기 작업이 쓰기 작업을 차단하지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. pg 패키지 설치 및 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 PostgreSQL을 사용하려면 node-postgres(pg) 패키지를 설치합니다. 이 패키지는 PostgreSQL 공식 프로토콜을 구현한 순수 JavaScript 클라이언트입니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install pg&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript를 사용하는 경우 타입 정의도 함께 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install pg @types/pg&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 연결 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const { Client } = require('pg');

const client = new Client({
  host: 'localhost',
  port: 5432,
  database: 'mydb',
  user: 'postgres',
  password: 'password'
});

async function connect() {
  try {
    await client.connect();
    console.log('PostgreSQL 연결 성공');

    const result = await client.query('SELECT NOW()');
    console.log('현재 시간:', result.rows[0].now);
  } catch (error) {
    console.error('연결 오류:', error.message);
  } finally {
    await client.end();
  }
}

connect();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경 변수를 활용한 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안을 위해 데이터베이스 접속 정보는 환경 변수로 관리하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const { Client } = require('pg');

const client = new Client({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 연결 풀(Pool) 구성과 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애플리케이션에서는 Client 대신 Pool을 사용합니다. Pool은 여러 클라이언트 연결을 관리하며, 요청마다 새 연결을 생성하는 오버헤드를 줄여줍니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const { Pool } = require('pg');

const pool = new Pool({
  host: 'localhost',
  port: 5432,
  database: 'mydb',
  user: 'postgres',
  password: 'password',
  max: 20,                      // 최대 연결 수
  idleTimeoutMillis: 30000,     // 유휴 연결 타임아웃 (30초)
  connectionTimeoutMillis: 2000 // 연결 타임아웃 (2초)
});

// 풀 이벤트 리스너
pool.on('connect', () =&amp;gt; {
  console.log('새 클라이언트 연결됨');
});

pool.on('error', (err) =&amp;gt; {
  console.error('풀 오류:', err.message);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;풀을 사용한 쿼리 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function queryWithPool() {
  try {
    // 방법 1: pool.query() - 간단한 쿼리에 적합
    const result = await pool.query('SELECT * FROM users WHERE id = $1', [1]);
    console.log(result.rows);

    // 방법 2: 클라이언트 체크아웃 - 여러 쿼리를 순차 실행할 때
    const client = await pool.connect();
    try {
      const res1 = await client.query('SELECT * FROM users');
      const res2 = await client.query('SELECT * FROM orders');
      return { users: res1.rows, orders: res2.rows };
    } finally {
      client.release(); // 반드시 클라이언트 반환
    }
  } catch (error) {
    console.error('쿼리 오류:', error.message);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션 종료 시 풀 정리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;process.on('SIGINT', async () =&amp;gt; {
  await pool.end();
  console.log('풀 연결 종료');
  process.exit(0);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파라미터화된 쿼리(Parameterized Query)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 인젝션 공격을 방지하려면 반드시 파라미터화된 쿼리를 사용해야 합니다. pg 패키지는 $1, $2 형식의 위치 기반 파라미터를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;잘못된 방법 (SQL 인젝션 취약)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;// 절대 사용하지 마세요
const query = `SELECT * FROM users WHERE name = '${userName}'`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;올바른 방법 (파라미터화된 쿼리)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async function findUserByName(name) {
  const query = 'SELECT * FROM users WHERE name = $1';
  const values = [name];

  const result = await pool.query(query, values);
  return result.rows;
}

async function insertUser(name, email, age) {
  const query = `
    INSERT INTO users (name, email, age)
    VALUES ($1, $2, $3)
    RETURNING id, name, email, age, created_at
  `;
  const values = [name, email, age];

  const result = await pool.query(query, values);
  return result.rows[0];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Named Parameters 스타일 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg-format 또는 sql-template-strings 패키지를 사용하면 더 직관적인 쿼리 작성이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const format = require('pg-format');

async function findUsers(names) {
  // 배열 값을 안전하게 처리
  const query = format('SELECT * FROM users WHERE name IN (%L)', names);
  const result = await pool.query(query);
  return result.rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CRUD 작업 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 자주 사용하는 CRUD(Create, Read, Update, Delete) 작업을 구현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테이블 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;async function createTable() {
  const query = `
    CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      name VARCHAR(100) NOT NULL,
      email VARCHAR(255) UNIQUE NOT NULL,
      age INTEGER,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
  `;

  await pool.query(query);
  console.log('users 테이블 생성 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Create - 데이터 삽입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async function createUser(userData) {
  const { name, email, age } = userData;
  const query = `
    INSERT INTO users (name, email, age)
    VALUES ($1, $2, $3)
    RETURNING *
  `;

  const result = await pool.query(query, [name, email, age]);
  return result.rows[0];
}

// 다중 삽입
async function createUsers(usersData) {
  const query = `
    INSERT INTO users (name, email, age)
    SELECT * FROM UNNEST($1::varchar[], $2::varchar[], $3::int[])
    RETURNING *
  `;

  const names = usersData.map(u =&amp;gt; u.name);
  const emails = usersData.map(u =&amp;gt; u.email);
  const ages = usersData.map(u =&amp;gt; u.age);

  const result = await pool.query(query, [names, emails, ages]);
  return result.rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Read - 데이터 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getUserById(id) {
  const query = 'SELECT * FROM users WHERE id = $1';
  const result = await pool.query(query, [id]);
  return result.rows[0] || null;
}

async function getUsers(options = {}) {
  const { limit = 10, offset = 0, orderBy = 'created_at', order = 'DESC' } = options;

  // orderBy와 order는 허용된 값만 사용 (SQL 인젝션 방지)
  const allowedColumns = ['id', 'name', 'email', 'created_at'];
  const allowedOrders = ['ASC', 'DESC'];

  const safeOrderBy = allowedColumns.includes(orderBy) ? orderBy : 'created_at';
  const safeOrder = allowedOrders.includes(order.toUpperCase()) ? order.toUpperCase() : 'DESC';

  const query = `
    SELECT * FROM users
    ORDER BY ${safeOrderBy} ${safeOrder}
    LIMIT $1 OFFSET $2
  `;

  const result = await pool.query(query, [limit, offset]);
  return result.rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Update - 데이터 수정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function updateUser(id, updates) {
  const fields = [];
  const values = [];
  let paramIndex = 1;

  Object.keys(updates).forEach(key =&amp;gt; {
    if (updates[key] !== undefined) {
      fields.push(`${key} = $${paramIndex}`);
      values.push(updates[key]);
      paramIndex++;
    }
  });

  if (fields.length === 0) {
    throw new Error('수정할 필드가 없습니다');
  }

  fields.push(`updated_at = CURRENT_TIMESTAMP`);
  values.push(id);

  const query = `
    UPDATE users
    SET ${fields.join(', ')}
    WHERE id = $${paramIndex}
    RETURNING *
  `;

  const result = await pool.query(query, values);
  return result.rows[0];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Delete - 데이터 삭제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function deleteUser(id) {
  const query = 'DELETE FROM users WHERE id = $1 RETURNING *';
  const result = await pool.query(query, [id]);
  return result.rows[0] || null;
}

async function deleteUsersByAge(minAge) {
  const query = 'DELETE FROM users WHERE age &amp;lt; $1 RETURNING id';
  const result = await pool.query(query, [minAge]);
  return result.rowCount; // 삭제된 행 수
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 트랜잭션 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 쿼리를 하나의 작업 단위로 처리해야 할 때 트랜잭션을 사용합니다. 트랜잭션 내의 모든 쿼리가 성공해야 커밋되고, 하나라도 실패하면 전체가 롤백됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 트랜잭션 패턴&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function transferMoney(fromId, toId, amount) {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');

    // 출금 계좌에서 차감
    const withdrawQuery = `
      UPDATE accounts SET balance = balance - $1
      WHERE id = $2 AND balance &amp;gt;= $1
      RETURNING balance
    `;
    const withdrawResult = await client.query(withdrawQuery, [amount, fromId]);

    if (withdrawResult.rowCount === 0) {
      throw new Error('잔액이 부족하거나 계좌가 존재하지 않습니다');
    }

    // 입금 계좌에 추가
    const depositQuery = `
      UPDATE accounts SET balance = balance + $1
      WHERE id = $2
      RETURNING balance
    `;
    const depositResult = await client.query(depositQuery, [amount, toId]);

    if (depositResult.rowCount === 0) {
      throw new Error('입금 계좌가 존재하지 않습니다');
    }

    // 거래 기록 저장
    await client.query(
      'INSERT INTO transactions (from_id, to_id, amount) VALUES ($1, $2, $3)',
      [fromId, toId, amount]
    );

    await client.query('COMMIT');

    return {
      success: true,
      fromBalance: withdrawResult.rows[0].balance,
      toBalance: depositResult.rows[0].balance
    };
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 헬퍼 함수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복적인 트랜잭션 패턴을 헬퍼 함수로 추상화하면 코드가 간결해집니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;async function withTransaction(callback) {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');
    const result = await callback(client);
    await client.query('COMMIT');
    return result;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

// 사용 예시
async function createOrderWithItems(orderData, items) {
  return withTransaction(async (client) =&amp;gt; {
    // 주문 생성
    const orderResult = await client.query(
      'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
      [orderData.userId, orderData.total]
    );
    const orderId = orderResult.rows[0].id;

    // 주문 항목 추가
    for (const item of items) {
      await client.query(
        'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)',
        [orderId, item.productId, item.quantity, item.price]
      );

      // 재고 감소
      await client.query(
        'UPDATE products SET stock = stock - $1 WHERE id = $2',
        [item.quantity, item.productId]
      );
    }

    return orderId;
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. JSON/JSONB 데이터 타입 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 JSON과 JSONB 데이터 타입을 지원합니다. JSONB는 바이너리 형태로 저장되어 인덱싱과 검색 성능이 우수합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSONB 컬럼이 있는 테이블 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async function createProductTable() {
  const query = `
    CREATE TABLE IF NOT EXISTS products (
      id SERIAL PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      price DECIMAL(10, 2) NOT NULL,
      metadata JSONB DEFAULT '{}',
      tags JSONB DEFAULT '[]',
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
  `;

  await pool.query(query);

  // JSONB 컬럼에 GIN 인덱스 생성 (검색 성능 향상)
  await pool.query(`
    CREATE INDEX IF NOT EXISTS idx_products_metadata ON products USING GIN (metadata)
  `);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSONB 데이터 삽입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async function createProduct(productData) {
  const { name, price, metadata, tags } = productData;

  const query = `
    INSERT INTO products (name, price, metadata, tags)
    VALUES ($1, $2, $3, $4)
    RETURNING *
  `;

  const result = await pool.query(query, [
    name,
    price,
    JSON.stringify(metadata),
    JSON.stringify(tags)
  ]);

  return result.rows[0];
}

// 사용 예시
await createProduct({
  name: '무선 이어폰',
  price: 89000,
  metadata: {
    brand: 'TechBrand',
    color: 'black',
    specs: {
      battery: '24시간',
      bluetooth: '5.0',
      weight: '5g'
    }
  },
  tags: ['전자기기', '오디오', '무선']
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSONB 쿼리 연산자&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 특정 키 값으로 검색 (-&amp;gt; 연산자: JSON 객체 반환)
async function getProductsByBrand(brand) {
  const query = `
    SELECT * FROM products
    WHERE metadata-&amp;gt;&amp;gt;'brand' = $1
  `;
  const result = await pool.query(query, [brand]);
  return result.rows;
}

// 중첩된 값 검색 (#&amp;gt;&amp;gt; 연산자: 경로로 텍스트 값 추출)
async function getProductsByBluetooth(version) {
  const query = `
    SELECT * FROM products
    WHERE metadata#&amp;gt;&amp;gt;'{specs,bluetooth}' = $1
  `;
  const result = await pool.query(query, [version]);
  return result.rows;
}

// 키 존재 여부 확인 (? 연산자)
async function getProductsWithColor() {
  const query = `
    SELECT * FROM products
    WHERE metadata ? 'color'
  `;
  const result = await pool.query(query);
  return result.rows;
}

// 배열에 값 포함 여부 (@&amp;gt; 연산자: 포함 관계)
async function getProductsByTag(tag) {
  const query = `
    SELECT * FROM products
    WHERE tags @&amp;gt; $1::jsonb
  `;
  const result = await pool.query(query, [JSON.stringify([tag])]);
  return result.rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSONB 데이터 업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 특정 키 값 업데이트 (jsonb_set 함수)
async function updateProductColor(id, color) {
  const query = `
    UPDATE products
    SET metadata = jsonb_set(metadata, '{color}', $1::jsonb)
    WHERE id = $2
    RETURNING *
  `;
  const result = await pool.query(query, [JSON.stringify(color), id]);
  return result.rows[0];
}

// 여러 키 병합 (|| 연산자)
async function mergeProductMetadata(id, newMetadata) {
  const query = `
    UPDATE products
    SET metadata = metadata || $1::jsonb
    WHERE id = $2
    RETURNING *
  `;
  const result = await pool.query(query, [JSON.stringify(newMetadata), id]);
  return result.rows[0];
}

// 키 삭제 (- 연산자)
async function removeProductMetadataKey(id, key) {
  const query = `
    UPDATE products
    SET metadata = metadata - $1
    WHERE id = $2
    RETURNING *
  `;
  const result = await pool.query(query, [key, id]);
  return result.rows[0];
}

// 배열에 요소 추가
async function addProductTag(id, tag) {
  const query = `
    UPDATE products
    SET tags = tags || $1::jsonb
    WHERE id = $2 AND NOT tags @&amp;gt; $1::jsonb
    RETURNING *
  `;
  const result = await pool.query(query, [JSON.stringify([tag]), id]);
  return result.rows[0];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JSONB 집계 함수&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 모든 고유 태그 추출
async function getAllUniqueTags() {
  const query = `
    SELECT DISTINCT jsonb_array_elements_text(tags) as tag
    FROM products
    ORDER BY tag
  `;
  const result = await pool.query(query);
  return result.rows.map(row =&amp;gt; row.tag);
}

// 브랜드별 상품 수 집계
async function countByBrand() {
  const query = `
    SELECT metadata-&amp;gt;&amp;gt;'brand' as brand, COUNT(*) as count
    FROM products
    WHERE metadata ? 'brand'
    GROUP BY metadata-&amp;gt;&amp;gt;'brand'
    ORDER BY count DESC
  `;
  const result = await pool.query(query);
  return result.rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 PostgreSQL을 연동할 때는 pg 패키지의 Pool을 사용하여 연결을 효율적으로 관리하고, 파라미터화된 쿼리로 SQL 인젝션을 방지해야 합니다. 트랜잭션은 데이터 무결성이 중요한 작업에서 필수이며, JSONB 타입을 활용하면 유연한 데이터 구조를 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/826</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-PostgreSQL-%EC%97%B0%EB%8F%99#entry826comment</comments>
      <pubDate>Thu, 19 Mar 2026 08:00:48 +0900</pubDate>
    </item>
    <item>
      <title>Node.js Sequelize ORM 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-Sequelize-ORM-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Sequelize ORM 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequelize는 Node.js 환경에서 가장 널리 사용되는 Promise 기반 ORM(Object-Relational Mapping) 라이브러리입니다. PostgreSQL, MySQL, MariaDB, SQLite, Microsoft SQL Server를 지원하며, JavaScript 객체를 통해 데이터베이스를 조작할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequelize를 사용하면 SQL 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있습니다. 모델 정의, 관계 설정, 트랜잭션 처리 등 데이터베이스 관련 작업을 객체 지향적으로 처리할 수 있어 코드의 가독성과 유지보수성이 향상됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 설치 및 초기 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 패키지 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;# Sequelize 코어 패키지 설치
npm install sequelize

# 사용하는 데이터베이스에 맞는 드라이버 설치
npm install pg pg-hstore      # PostgreSQL
npm install mysql2            # MySQL
npm install mariadb           # MariaDB
npm install sqlite3           # SQLite
npm install tedious           # Microsoft SQL Server&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Sequelize CLI 설치&lt;/h3&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;npm install --save-dev sequelize-cli&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 프로젝트 초기화&lt;/h3&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;npx sequelize-cli init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어를 실행하면 다음 폴더 구조가 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;project/
├── config/
│   └── config.json       # 데이터베이스 연결 설정
├── models/
│   └── index.js          # 모델 로더
├── migrations/           # 마이그레이션 파일
└── seeders/              # 시드 파일&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 데이터베이스 연결 설정&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// config/config.json
{
  &quot;development&quot;: {
    &quot;username&quot;: &quot;root&quot;,
    &quot;password&quot;: &quot;password&quot;,
    &quot;database&quot;: &quot;myapp_dev&quot;,
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;dialect&quot;: &quot;mysql&quot;,
    &quot;logging&quot;: console.log
  },
  &quot;production&quot;: {
    &quot;username&quot;: &quot;root&quot;,
    &quot;password&quot;: &quot;password&quot;,
    &quot;database&quot;: &quot;myapp_prod&quot;,
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;dialect&quot;: &quot;mysql&quot;,
    &quot;logging&quot;: false
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.5 Sequelize 인스턴스 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { Sequelize } = require('sequelize');

// 방법 1: 개별 파라미터 전달
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'mysql',
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
});

// 방법 2: 연결 URI 사용
const sequelize = new Sequelize('mysql://user:password@localhost:3306/database');

// 연결 테스트
async function testConnection() {
  try {
    await sequelize.authenticate();
    console.log('데이터베이스 연결 성공');
  } catch (error) {
    console.error('데이터베이스 연결 실패:', error);
  }
}

testConnection();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 모델 정의와 데이터 타입&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 모델 정의&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const { DataTypes } = require('sequelize');

const User = sequelize.define('User', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  username: {
    type: DataTypes.STRING(50),
    allowNull: false,
    unique: true
  },
  email: {
    type: DataTypes.STRING(100),
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  password: {
    type: DataTypes.STRING(255),
    allowNull: false
  },
  age: {
    type: DataTypes.INTEGER,
    validate: {
      min: 0,
      max: 150
    }
  },
  isActive: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  },
  createdAt: {
    type: DataTypes.DATE,
    defaultValue: DataTypes.NOW
  }
}, {
  tableName: 'users',
  timestamps: true,
  underscored: true
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 주요 데이터 타입&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;DataTypes.STRING          // VARCHAR(255)
DataTypes.STRING(100)     // VARCHAR(100)
DataTypes.TEXT            // TEXT
DataTypes.TEXT('tiny')    // TINYTEXT

DataTypes.INTEGER         // INTEGER
DataTypes.BIGINT          // BIGINT
DataTypes.FLOAT           // FLOAT
DataTypes.DOUBLE          // DOUBLE
DataTypes.DECIMAL(10, 2)  // DECIMAL(10, 2)

DataTypes.BOOLEAN         // BOOLEAN
DataTypes.DATE            // DATETIME
DataTypes.DATEONLY        // DATE (날짜만)

DataTypes.JSON            // JSON
DataTypes.JSONB           // JSONB (PostgreSQL 전용)
DataTypes.ARRAY(DataTypes.STRING)  // 배열 (PostgreSQL 전용)

DataTypes.UUID            // UUID
DataTypes.UUIDV4          // UUID v4
DataTypes.ENUM('value1', 'value2')  // ENUM&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 검증(Validation) 옵션&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const Product = sequelize.define('Product', {
  name: {
    type: DataTypes.STRING,
    validate: {
      notEmpty: true,
      len: [2, 100]
    }
  },
  price: {
    type: DataTypes.DECIMAL(10, 2),
    validate: {
      isDecimal: true,
      min: 0
    }
  },
  email: {
    type: DataTypes.STRING,
    validate: {
      isEmail: {
        msg: '유효한 이메일 주소를 입력하세요'
      }
    }
  },
  website: {
    type: DataTypes.STRING,
    validate: {
      isUrl: true
    }
  },
  status: {
    type: DataTypes.STRING,
    validate: {
      isIn: [['active', 'inactive', 'pending']]
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 관계 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 1:1 관계 (hasOne, belongsTo)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const User = sequelize.define('User', {
  name: DataTypes.STRING
});

const Profile = sequelize.define('Profile', {
  bio: DataTypes.TEXT,
  website: DataTypes.STRING
});

// User는 하나의 Profile을 가짐
User.hasOne(Profile, {
  foreignKey: 'userId',
  as: 'profile',
  onDelete: 'CASCADE'
});

// Profile은 하나의 User에 속함
Profile.belongsTo(User, {
  foreignKey: 'userId',
  as: 'user'
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 1:N 관계 (hasMany, belongsTo)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const User = sequelize.define('User', {
  name: DataTypes.STRING
});

const Post = sequelize.define('Post', {
  title: DataTypes.STRING,
  content: DataTypes.TEXT
});

// User는 여러 Post를 가짐
User.hasMany(Post, {
  foreignKey: 'authorId',
  as: 'posts'
});

// Post는 하나의 User에 속함
Post.belongsTo(User, {
  foreignKey: 'authorId',
  as: 'author'
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 N:M 관계 (belongsToMany)&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;const Post = sequelize.define('Post', {
  title: DataTypes.STRING
});

const Tag = sequelize.define('Tag', {
  name: DataTypes.STRING
});

// 중간 테이블 정의
const PostTag = sequelize.define('PostTag', {
  createdAt: DataTypes.DATE
});

// 다대다 관계 설정
Post.belongsToMany(Tag, {
  through: PostTag,
  foreignKey: 'postId',
  as: 'tags'
});

Tag.belongsToMany(Post, {
  through: PostTag,
  foreignKey: 'tagId',
  as: 'posts'
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 관계 데이터 조회&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 연관 데이터 포함 조회
const userWithPosts = await User.findOne({
  where: { id: 1 },
  include: [
    {
      model: Post,
      as: 'posts',
      include: [
        {
          model: Tag,
          as: 'tags'
        }
      ]
    },
    {
      model: Profile,
      as: 'profile'
    }
  ]
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CRUD 작업과 쿼리 빌더&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 Create (생성)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 단일 레코드 생성
const user = await User.create({
  username: 'john_doe',
  email: 'john@example.com',
  password: 'hashedpassword'
});

// 여러 레코드 일괄 생성
const users = await User.bulkCreate([
  { username: 'user1', email: 'user1@example.com', password: 'pass1' },
  { username: 'user2', email: 'user2@example.com', password: 'pass2' }
], {
  validate: true  // 검증 활성화
});

// findOrCreate - 있으면 조회, 없으면 생성
const [user, created] = await User.findOrCreate({
  where: { email: 'john@example.com' },
  defaults: {
    username: 'john_doe',
    password: 'hashedpassword'
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 Read (조회)&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// 전체 조회
const users = await User.findAll();

// 조건 조회
const { Op } = require('sequelize');

const activeUsers = await User.findAll({
  where: {
    isActive: true,
    age: {
      [Op.gte]: 18,       // &amp;gt;= 18
      [Op.lt]: 65         // &amp;lt; 65
    }
  },
  order: [['createdAt', 'DESC']],
  limit: 10,
  offset: 0
});

// 단일 레코드 조회
const user = await User.findOne({
  where: { email: 'john@example.com' }
});

// PK로 조회
const user = await User.findByPk(1);

// 집계 함수
const count = await User.count({
  where: { isActive: true }
});

const maxAge = await User.max('age');
const minAge = await User.min('age');
const sumAge = await User.sum('age');

// 그룹화
const usersByStatus = await User.findAll({
  attributes: [
    'isActive',
    [sequelize.fn('COUNT', sequelize.col('id')), 'count']
  ],
  group: ['isActive']
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 연산자 목록&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;const { Op } = require('sequelize');

// 비교 연산자
[Op.eq]: 3              // = 3
[Op.ne]: 20             // != 20
[Op.gt]: 6              // &amp;gt; 6
[Op.gte]: 6             // &amp;gt;= 6
[Op.lt]: 10             // &amp;lt; 10
[Op.lte]: 10            // &amp;lt;= 10
[Op.between]: [6, 10]   // BETWEEN 6 AND 10
[Op.notBetween]: [11, 15]

// 문자열 연산자
[Op.like]: '%hat'       // LIKE '%hat'
[Op.notLike]: '%hat'
[Op.startsWith]: 'hat'  // LIKE 'hat%'
[Op.endsWith]: 'hat'    // LIKE '%hat'
[Op.substring]: 'hat'   // LIKE '%hat%'

// 배열 연산자
[Op.in]: [1, 2]         // IN (1, 2)
[Op.notIn]: [1, 2]

// 논리 연산자
[Op.and]: [{ a: 5 }, { b: 6 }]
[Op.or]: [{ a: 5 }, { b: 6 }]
[Op.not]: true

// NULL 체크
[Op.is]: null           // IS NULL
[Op.not]: null          // IS NOT NULL&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 Update (수정)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 단일 레코드 수정
const user = await User.findByPk(1);
user.username = 'new_username';
await user.save();

// update 메서드 사용
await user.update({
  username: 'new_username',
  email: 'new@example.com'
});

// 조건에 맞는 레코드 일괄 수정
await User.update(
  { isActive: false },
  {
    where: {
      lastLogin: {
        [Op.lt]: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)  // 30일 이전
      }
    }
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.5 Delete (삭제)&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 단일 레코드 삭제
const user = await User.findByPk(1);
await user.destroy();

// 조건에 맞는 레코드 일괄 삭제
await User.destroy({
  where: {
    isActive: false
  }
});

// 소프트 삭제 (paranoid 옵션 사용 시)
const User = sequelize.define('User', {
  // 필드 정의
}, {
  paranoid: true  // deletedAt 컬럼 자동 추가
});

// 소프트 삭제된 레코드 포함 조회
await User.findAll({ paranoid: false });

// 소프트 삭제 레코드 복구
await user.restore();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마이그레이션 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 마이그레이션 파일 생성&lt;/h3&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;npx sequelize-cli migration:generate --name create-users-table&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 마이그레이션 파일 작성&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// migrations/20240101000000-create-users-table.js
'use strict';

module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable('users', {
      id: {
        type: Sequelize.INTEGER,
        primaryKey: true,
        autoIncrement: true
      },
      username: {
        type: Sequelize.STRING(50),
        allowNull: false,
        unique: true
      },
      email: {
        type: Sequelize.STRING(100),
        allowNull: false,
        unique: true
      },
      password: {
        type: Sequelize.STRING(255),
        allowNull: false
      },
      is_active: {
        type: Sequelize.BOOLEAN,
        defaultValue: true
      },
      created_at: {
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
      },
      updated_at: {
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
      }
    });

    // 인덱스 추가
    await queryInterface.addIndex('users', ['email']);
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('users');
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 컬럼 추가/수정/삭제 마이그레이션&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// migrations/20240102000000-add-phone-to-users.js
'use strict';

module.exports = {
  async up(queryInterface, Sequelize) {
    // 컬럼 추가
    await queryInterface.addColumn('users', 'phone', {
      type: Sequelize.STRING(20),
      allowNull: true
    });

    // 컬럼 수정
    await queryInterface.changeColumn('users', 'username', {
      type: Sequelize.STRING(100),
      allowNull: false
    });
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.removeColumn('users', 'phone');
    await queryInterface.changeColumn('users', 'username', {
      type: Sequelize.STRING(50),
      allowNull: false
    });
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 마이그레이션 실행 명령어&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# 마이그레이션 실행
npx sequelize-cli db:migrate

# 마이그레이션 되돌리기 (최근 1개)
npx sequelize-cli db:migrate:undo

# 모든 마이그레이션 되돌리기
npx sequelize-cli db:migrate:undo:all

# 특정 마이그레이션까지 되돌리기
npx sequelize-cli db:migrate:undo:all --to 20240101000000-create-users-table.js

# 마이그레이션 상태 확인
npx sequelize-cli db:migrate:status&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 트랜잭션과 훅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 트랜잭션 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 관리형 트랜잭션 (자동 커밋/롤백)
const result = await sequelize.transaction(async (t) =&amp;gt; {
  const user = await User.create({
    username: 'john',
    email: 'john@example.com',
    password: 'password'
  }, { transaction: t });

  await Profile.create({
    userId: user.id,
    bio: 'Hello, I am John'
  }, { transaction: t });

  return user;
});

// 비관리형 트랜잭션 (수동 커밋/롤백)
const t = await sequelize.transaction();

try {
  const user = await User.create({
    username: 'jane',
    email: 'jane@example.com',
    password: 'password'
  }, { transaction: t });

  await Post.create({
    title: 'First Post',
    content: 'Hello World',
    authorId: user.id
  }, { transaction: t });

  await t.commit();
} catch (error) {
  await t.rollback();
  throw error;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 트랜잭션 격리 수준&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const { Transaction } = require('sequelize');

await sequelize.transaction({
  isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
}, async (t) =&amp;gt; {
  // 트랜잭션 작업
});

// 격리 수준 종류
Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED
Transaction.ISOLATION_LEVELS.READ_COMMITTED
Transaction.ISOLATION_LEVELS.REPEATABLE_READ
Transaction.ISOLATION_LEVELS.SERIALIZABLE&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 훅(Hooks) 정의&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const User = sequelize.define('User', {
  username: DataTypes.STRING,
  email: DataTypes.STRING,
  password: DataTypes.STRING
}, {
  hooks: {
    // 생성 전 훅
    beforeCreate: async (user, options) =&amp;gt; {
      const bcrypt = require('bcrypt');
      user.password = await bcrypt.hash(user.password, 10);
    },

    // 생성 후 훅
    afterCreate: async (user, options) =&amp;gt; {
      console.log(`새 사용자 생성: ${user.username}`);
    },

    // 수정 전 훅
    beforeUpdate: async (user, options) =&amp;gt; {
      if (user.changed('password')) {
        const bcrypt = require('bcrypt');
        user.password = await bcrypt.hash(user.password, 10);
      }
    },

    // 삭제 전 훅
    beforeDestroy: async (user, options) =&amp;gt; {
      await Post.destroy({
        where: { authorId: user.id }
      });
    },

    // 검증 후 훅
    afterValidate: async (user, options) =&amp;gt; {
      user.email = user.email.toLowerCase();
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 훅 종류&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;// 생성 관련
beforeCreate, afterCreate
beforeBulkCreate, afterBulkCreate

// 수정 관련
beforeUpdate, afterUpdate
beforeBulkUpdate, afterBulkUpdate
beforeSave, afterSave  // create와 update 모두에 적용

// 삭제 관련
beforeDestroy, afterDestroy
beforeBulkDestroy, afterBulkDestroy

// 조회 관련
beforeFind, afterFind

// 검증 관련
beforeValidate, afterValidate

// 동기화 관련
beforeSync, afterSync&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.5 훅 개별 추가&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// addHook 메서드 사용
User.addHook('beforeCreate', 'hashPassword', async (user) =&amp;gt; {
  const bcrypt = require('bcrypt');
  user.password = await bcrypt.hash(user.password, 10);
});

// 전역 훅 설정
const sequelize = new Sequelize('database', 'username', 'password', {
  dialect: 'mysql',
  hooks: {
    beforeCreate: async (instance) =&amp;gt; {
      console.log('레코드 생성 시작');
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequelize는 Node.js 애플리케이션에서 데이터베이스 작업을 효율적으로 처리할 수 있게 해주는 강력한 ORM입니다. 모델 정의, 관계 설정, 마이그레이션, 트랜잭션 등 데이터베이스 관련 기능을 객체 지향적으로 관리할 수 있어 코드의 가독성과 유지보수성을 높여줍니다. 실제 프로젝트에서는 마이그레이션을 통한 스키마 버전 관리와 트랜잭션을 통한 데이터 무결성 보장을 적극 활용하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/825</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-Sequelize-ORM-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry825comment</comments>
      <pubDate>Wed, 18 Mar 2026 20:00:31 +0900</pubDate>
    </item>
    <item>
      <title>Node.js MySQL 연동 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-MySQL-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션에서 MySQL 데이터베이스를 연동하는 방법을 알아봅니다. mysql2 패키지를 중심으로 연결 설정부터 CRUD 작업, 트랜잭션 처리까지 실무에서 바로 사용할 수 있는 내용을 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. mysql2 패키지 소개&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mysql vs mysql2 패키지 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 MySQL을 연동할 때 주로 사용하는 패키지는 &lt;code&gt;mysql&lt;/code&gt;과 &lt;code&gt;mysql2&lt;/code&gt; 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mysql2를 선택해야 하는 이유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Promise 네이티브 지원: mysql2는 &lt;code&gt;.promise()&lt;/code&gt; 메서드를 통해 Promise 기반 API를 기본 제공합니다&lt;/li&gt;
&lt;li&gt;Prepared Statement 지원: 서버 사이드 Prepared Statement를 지원하여 보안과 성능이 향상됩니다&lt;/li&gt;
&lt;li&gt;더 빠른 성능: mysql 패키지 대비 파싱 속도가 개선되었습니다&lt;/li&gt;
&lt;li&gt;mysql 패키지 호환성: 기존 mysql 패키지와 API가 호환되어 마이그레이션이 쉽습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// mysql 패키지 (콜백 기반)
const mysql = require('mysql');

// mysql2 패키지 (Promise 지원)
const mysql = require('mysql2/promise');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 설치 및 기본 연결 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install mysql2&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 연결 설정&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const mysql = require('mysql2/promise');

async function connectDatabase() {
  const connection = await mysql.createConnection({
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: 'your_password',
    database: 'your_database'
  });

  console.log('MySQL 연결 성공');
  return connection;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 옵션 상세&lt;/h3&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const connectionConfig = {
  host: 'localhost',           // MySQL 서버 호스트
  port: 3306,                  // MySQL 포트 (기본값: 3306)
  user: 'root',                // 사용자명
  password: 'password',        // 비밀번호
  database: 'mydb',            // 데이터베이스명
  charset: 'utf8mb4',          // 문자셋 (이모지 지원 시 utf8mb4)
  timezone: '+09:00',          // 타임존 설정
  connectTimeout: 10000,       // 연결 타임아웃 (ms)
  waitForConnections: true,    // 연결 대기 여부
  dateStrings: true            // 날짜를 문자열로 반환
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 연결 풀(Connection Pool) 사용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 연결 대신 연결 풀을 사용하면 여러 요청을 효율적으로 처리할 수 있습니다. 실무에서는 연결 풀 사용을 권장합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 풀 생성&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'your_password',
  database: 'your_database',
  waitForConnections: true,
  connectionLimit: 10,         // 최대 연결 수
  queueLimit: 0,               // 대기열 제한 (0 = 무제한)
  enableKeepAlive: true,       // Keep-Alive 활성화
  keepAliveInitialDelay: 0     // Keep-Alive 초기 지연
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 풀 사용 예제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 방법 1: pool.query() 직접 사용 (권장)
async function getUsers() {
  const [rows] = await pool.query('SELECT * FROM users');
  return rows;
}

// 방법 2: connection 획득 후 사용
async function getUserById(id) {
  const connection = await pool.getConnection();
  try {
    const [rows] = await connection.query(
      'SELECT * FROM users WHERE id = ?',
      [id]
    );
    return rows[0];
  } finally {
    connection.release(); // 반드시 연결 반환
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연결 풀의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결 재사용으로 오버헤드 감소&lt;/li&gt;
&lt;li&gt;동시 요청 처리 능력 향상&lt;/li&gt;
&lt;li&gt;자동 연결 관리 (끊어진 연결 재생성)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Prepared Statement와 SQL Injection 방지&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL Injection 위험 예시&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 위험한 코드 - SQL Injection 취약
const userId = &quot;1 OR 1=1&quot;; // 악의적인 입력
const query = `SELECT * FROM users WHERE id = ${userId}`;
// 결과: SELECT * FROM users WHERE id = 1 OR 1=1 (모든 데이터 노출)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Prepared Statement 사용 (안전한 방법)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 안전한 코드 - Placeholder 사용
async function getUserSafe(userId) {
  const [rows] = await pool.query(
    'SELECT * FROM users WHERE id = ?',
    [userId]
  );
  return rows;
}

// 여러 파라미터 사용
async function searchUsers(name, email) {
  const [rows] = await pool.query(
    'SELECT * FROM users WHERE name LIKE ? AND email = ?',
    [`%${name}%`, email]
  );
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Named Placeholder 사용&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;async function getUserByNamedParams(params) {
  const [rows] = await pool.query(
    'SELECT * FROM users WHERE name = :name AND age &amp;gt; :age',
    { name: params.name, age: params.age }
  );
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;execute() 메서드로 Prepared Statement 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// execute()는 서버 사이드 Prepared Statement 사용
async function getUserWithPrepared(userId) {
  const [rows] = await pool.execute(
    'SELECT * FROM users WHERE id = ?',
    [userId]
  );
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;query()&lt;/code&gt;와 &lt;code&gt;execute()&lt;/code&gt;의 차이점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;query()&lt;/code&gt;: 클라이언트 사이드에서 파라미터 이스케이프 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;execute()&lt;/code&gt;: 서버 사이드 Prepared Statement 사용 (동일 쿼리 반복 시 성능 이점)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CRUD 작업 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Create (생성)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function createUser(user) {
  const [result] = await pool.query(
    'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
    [user.name, user.email, user.age]
  );

  console.log('삽입된 ID:', result.insertId);
  console.log('영향받은 행:', result.affectedRows);

  return result.insertId;
}

// 여러 행 삽입
async function createUsers(users) {
  const values = users.map(u =&amp;gt; [u.name, u.email, u.age]);
  const [result] = await pool.query(
    'INSERT INTO users (name, email, age) VALUES ?',
    [values]
  );
  return result.affectedRows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Read (조회)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 전체 조회
async function getAllUsers() {
  const [rows] = await pool.query('SELECT * FROM users');
  return rows;
}

// 조건 조회
async function getUsersByAge(minAge) {
  const [rows] = await pool.query(
    'SELECT * FROM users WHERE age &amp;gt;= ? ORDER BY age DESC',
    [minAge]
  );
  return rows;
}

// 페이징 처리
async function getUsersWithPaging(page, limit) {
  const offset = (page - 1) * limit;
  const [rows] = await pool.query(
    'SELECT * FROM users LIMIT ? OFFSET ?',
    [limit, offset]
  );

  const [[{ total }]] = await pool.query(
    'SELECT COUNT(*) as total FROM users'
  );

  return {
    data: rows,
    total,
    page,
    totalPages: Math.ceil(total / limit)
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Update (수정)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function updateUser(id, updates) {
  const [result] = await pool.query(
    'UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?',
    [updates.name, updates.email, updates.age, id]
  );

  if (result.affectedRows === 0) {
    throw new Error('사용자를 찾을 수 없습니다');
  }

  return result.affectedRows;
}

// 동적 업데이트 (변경된 필드만)
async function updateUserPartial(id, updates) {
  const fields = [];
  const values = [];

  for (const [key, value] of Object.entries(updates)) {
    fields.push(`${key} = ?`);
    values.push(value);
  }

  if (fields.length === 0) {
    throw new Error('업데이트할 필드가 없습니다');
  }

  values.push(id);

  const [result] = await pool.query(
    `UPDATE users SET ${fields.join(', ')} WHERE id = ?`,
    values
  );

  return result.affectedRows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Delete (삭제)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function deleteUser(id) {
  const [result] = await pool.query(
    'DELETE FROM users WHERE id = ?',
    [id]
  );

  return result.affectedRows &amp;gt; 0;
}

// 조건부 삭제
async function deleteInactiveUsers(days) {
  const [result] = await pool.query(
    'DELETE FROM users WHERE last_login &amp;lt; DATE_SUB(NOW(), INTERVAL ? DAY)',
    [days]
  );

  return result.affectedRows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 트랜잭션 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 쿼리를 하나의 작업 단위로 처리해야 할 때 트랜잭션을 사용합니다. 모든 쿼리가 성공하면 커밋하고, 하나라도 실패하면 롤백합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 트랜잭션 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function transferMoney(fromId, toId, amount) {
  const connection = await pool.getConnection();

  try {
    await connection.beginTransaction();

    // 출금
    await connection.query(
      'UPDATE accounts SET balance = balance - ? WHERE id = ?',
      [amount, fromId]
    );

    // 입금
    await connection.query(
      'UPDATE accounts SET balance = balance + ? WHERE id = ?',
      [amount, toId]
    );

    // 거래 기록
    await connection.query(
      'INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)',
      [fromId, toId, amount]
    );

    await connection.commit();
    console.log('거래 완료');

  } catch (error) {
    await connection.rollback();
    console.error('거래 실패, 롤백됨:', error.message);
    throw error;

  } finally {
    connection.release();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 유틸리티 함수&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function withTransaction(callback) {
  const connection = await pool.getConnection();

  try {
    await connection.beginTransaction();
    const result = await callback(connection);
    await connection.commit();
    return result;

  } catch (error) {
    await connection.rollback();
    throw error;

  } finally {
    connection.release();
  }
}

// 사용 예시
async function createOrderWithItems(order, items) {
  return withTransaction(async (conn) =&amp;gt; {
    const [orderResult] = await conn.query(
      'INSERT INTO orders (user_id, total) VALUES (?, ?)',
      [order.userId, order.total]
    );

    const orderId = orderResult.insertId;

    for (const item of items) {
      await conn.query(
        'INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)',
        [orderId, item.productId, item.quantity]
      );
    }

    return orderId;
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Promise/async-await 패턴 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜백에서 Promise로 전환&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 콜백 방식 (레거시)
const mysql = require('mysql2');
const pool = mysql.createPool(config);

pool.query('SELECT * FROM users', (error, results) =&amp;gt; {
  if (error) {
    console.error(error);
    return;
  }
  console.log(results);
});

// Promise 방식 (권장)
const mysql = require('mysql2/promise');
const pool = mysql.createPool(config);

const [rows] = await pool.query('SELECT * FROM users');
console.log(rows);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병렬 쿼리 실행&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;async function getDashboardData(userId) {
  const [userInfo, orders, notifications] = await Promise.all([
    pool.query('SELECT * FROM users WHERE id = ?', [userId]),
    pool.query('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 10', [userId]),
    pool.query('SELECT * FROM notifications WHERE user_id = ? AND is_read = 0', [userId])
  ]);

  return {
    user: userInfo[0][0],
    recentOrders: orders[0],
    unreadNotifications: notifications[0]
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 핸들링&lt;/h3&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;async function safeQuery(query, params = []) {
  try {
    const [rows] = await pool.query(query, params);
    return { success: true, data: rows };

  } catch (error) {
    console.error('쿼리 실행 오류:', error.message);

    if (error.code === 'ER_DUP_ENTRY') {
      return { success: false, error: '중복된 데이터입니다' };
    }

    if (error.code === 'ER_NO_REFERENCED_ROW_2') {
      return { success: false, error: '참조하는 데이터가 존재하지 않습니다' };
    }

    return { success: false, error: '데이터베이스 오류가 발생했습니다' };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Express와 함께 사용하기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const mysql = require('mysql2/promise');

const app = express();
app.use(express.json());

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'mydb',
  connectionLimit: 10
});

// 사용자 목록 조회
app.get('/users', async (req, res) =&amp;gt; {
  try {
    const [rows] = await pool.query('SELECT * FROM users');
    res.json(rows);
  } catch (error) {
    res.status(500).json({ error: '서버 오류' });
  }
});

// 사용자 생성
app.post('/users', async (req, res) =&amp;gt; {
  try {
    const { name, email } = req.body;
    const [result] = await pool.query(
      'INSERT INTO users (name, email) VALUES (?, ?)',
      [name, email]
    );
    res.status(201).json({ id: result.insertId });
  } catch (error) {
    res.status(500).json({ error: '서버 오류' });
  }
});

app.listen(3000, () =&amp;gt; {
  console.log('서버 시작: http://localhost:3000');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 MySQL 연동 시 mysql2 패키지를 사용하면 Promise 지원과 Prepared Statement를 통해 안전하고 효율적인 데이터베이스 작업이 가능합니다. 실무에서는 연결 풀을 활용하고, Placeholder를 통한 SQL Injection 방지, 그리고 트랜잭션을 적절히 사용하여 데이터 무결성을 보장하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/824</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-MySQL-%EC%97%B0%EB%8F%99-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry824comment</comments>
      <pubDate>Wed, 18 Mar 2026 08:00:47 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Mongoose 사용법 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Mongoose-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Mongoose란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mongoose는 MongoDB를 위한 ODM(Object Data Modeling) 라이브러리입니다. ODM은 객체와 문서(Document) 간의 매핑을 담당하며, JavaScript 객체를 MongoDB 문서로 변환하고 그 반대의 작업도 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 스키마가 없는(Schema-less) NoSQL 데이터베이스이지만, Mongoose를 사용하면 애플리케이션 레벨에서 스키마를 정의할 수 있습니다. 이를 통해 데이터의 구조를 명확히 하고, 유효성 검사를 수행하며, 타입 변환을 자동으로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mongoose 설치 및 연결&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install mongoose&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/myapp')
  .then(() =&amp;gt; console.log('MongoDB 연결 성공'))
  .catch(err =&amp;gt; console.error('MongoDB 연결 실패:', err));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스키마(Schema) 정의와 타입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마는 MongoDB 컬렉션에 저장될 문서의 구조를 정의합니다. Mongoose는 다양한 스키마 타입을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지원하는 스키마 타입&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;String: 문자열&lt;/li&gt;
&lt;li&gt;Number: 숫자&lt;/li&gt;
&lt;li&gt;Date: 날짜&lt;/li&gt;
&lt;li&gt;Buffer: 바이너리 데이터&lt;/li&gt;
&lt;li&gt;Boolean: 불리언&lt;/li&gt;
&lt;li&gt;Mixed: 혼합 타입 (Schema.Types.Mixed)&lt;/li&gt;
&lt;li&gt;ObjectId: MongoDB ObjectId (Schema.Types.ObjectId)&lt;/li&gt;
&lt;li&gt;Array: 배열&lt;/li&gt;
&lt;li&gt;Decimal128: 고정밀 소수점&lt;/li&gt;
&lt;li&gt;Map: Map 객체&lt;/li&gt;
&lt;li&gt;UUID: UUID 타입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스키마 정의 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  age: {
    type: Number,
    min: 0,
    max: 150
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  tags: [String],
  profile: {
    bio: String,
    website: String
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스키마 옵션&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const schema = new Schema({ ... }, {
  timestamps: true,        // createdAt, updatedAt 자동 생성
  collection: 'users',     // 컬렉션 이름 지정
  versionKey: false,       // __v 필드 비활성화
  strict: true             // 스키마에 정의된 필드만 저장
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 모델(Model) 생성 및 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델은 스키마를 기반으로 생성되며, 실제 데이터베이스 작업을 수행하는 인터페이스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모델 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const User = mongoose.model('User', userSchema);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 생성 및 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 방법 1: new + save()
const user = new User({
  name: '홍길동',
  email: 'hong@example.com',
  age: 25
});
await user.save();

// 방법 2: create()
const user = await User.create({
  name: '김철수',
  email: 'kim@example.com',
  age: 30
});

// 방법 3: insertMany()
const users = await User.insertMany([
  { name: '이영희', email: 'lee@example.com' },
  { name: '박민수', email: 'park@example.com' }
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 전체 조회
const allUsers = await User.find();

// 조건 조회
const adults = await User.find({ age: { $gte: 18 } });

// 단일 문서 조회
const user = await User.findOne({ email: 'hong@example.com' });

// ID로 조회
const user = await User.findById('507f1f77bcf86cd799439011');

// 특정 필드만 선택
const users = await User.find().select('name email -_id');

// 정렬, 제한, 건너뛰기
const users = await User.find()
  .sort({ createdAt: -1 })
  .limit(10)
  .skip(20);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 수정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// findByIdAndUpdate
const updatedUser = await User.findByIdAndUpdate(
  userId,
  { name: '새이름' },
  { new: true, runValidators: true }
);

// updateOne
await User.updateOne(
  { email: 'hong@example.com' },
  { $set: { age: 26 } }
);

// updateMany
await User.updateMany(
  { role: 'user' },
  { $set: { isActive: true } }
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 삭제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// findByIdAndDelete
await User.findByIdAndDelete(userId);

// deleteOne
await User.deleteOne({ email: 'hong@example.com' });

// deleteMany
await User.deleteMany({ isActive: false });&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 유효성 검사(Validation)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mongoose는 스키마 레벨에서 강력한 유효성 검사 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내장 검사기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const productSchema = new Schema({
  name: {
    type: String,
    required: [true, '상품명은 필수입니다'],
    minlength: [2, '상품명은 2자 이상이어야 합니다'],
    maxlength: [100, '상품명은 100자 이하여야 합니다']
  },
  price: {
    type: Number,
    required: true,
    min: [0, '가격은 0 이상이어야 합니다']
  },
  category: {
    type: String,
    enum: {
      values: ['electronics', 'clothing', 'food'],
      message: '{VALUE}는 유효한 카테고리가 아닙니다'
    }
  },
  sku: {
    type: String,
    match: [/^[A-Z]{3}-\d{4}$/, 'SKU 형식이 올바르지 않습니다']
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커스텀 검사기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const userSchema = new Schema({
  email: {
    type: String,
    validate: {
      validator: function(v) {
        return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
      },
      message: props =&amp;gt; `${props.value}는 유효한 이메일이 아닙니다`
    }
  },
  phone: {
    type: String,
    validate: {
      validator: async function(v) {
        const count = await this.constructor.countDocuments({ phone: v });
        return count === 0;
      },
      message: '이미 등록된 전화번호입니다'
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유효성 검사 에러 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;try {
  await user.save();
} catch (error) {
  if (error.name === 'ValidationError') {
    for (const field in error.errors) {
      console.log(`${field}: ${error.errors[field].message}`);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 미들웨어(Middleware/Hooks)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미들웨어는 특정 작업 전후에 실행되는 함수입니다. pre(전)와 post(후) 훅으로 구분됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Document 미들웨어&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 저장 전 비밀번호 해싱
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();

  const bcrypt = require('bcrypt');
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// 저장 후 로깅
userSchema.post('save', function(doc) {
  console.log(`새 사용자 생성됨: ${doc.email}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Query 미들웨어&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// find 쿼리 전에 실행
userSchema.pre('find', function() {
  this.where({ isDeleted: false });
});

// findOne 쿼리 전에 실행
userSchema.pre('findOne', function() {
  this.where({ isDeleted: false });
});

// 업데이트 전 타임스탬프 갱신
userSchema.pre('findOneAndUpdate', function() {
  this.set({ updatedAt: new Date() });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Aggregate 미들웨어&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;userSchema.pre('aggregate', function() {
  this.pipeline().unshift({ $match: { isDeleted: false } });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 처리 미들웨어&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;userSchema.post('save', function(error, doc, next) {
  if (error.name === 'MongoServerError' &amp;amp;&amp;amp; error.code === 11000) {
    next(new Error('이미 존재하는 이메일입니다'));
  } else {
    next(error);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 쿼리 빌더와 Population&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 빌더&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mongoose는 체이닝 방식의 쿼리 빌더를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const results = await User.find()
  .where('age').gte(18).lte(65)
  .where('role').equals('user')
  .where('tags').in(['developer', 'designer'])
  .select('name email age')
  .sort('-createdAt')
  .limit(10)
  .lean()
  .exec();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 쿼리 메서드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where(): 조건 지정&lt;/li&gt;
&lt;li&gt;equals(): 값 일치&lt;/li&gt;
&lt;li&gt;gt(), gte(), lt(), lte(): 비교 연산&lt;/li&gt;
&lt;li&gt;in(), nin(): 포함 여부&lt;/li&gt;
&lt;li&gt;regex(): 정규식 매칭&lt;/li&gt;
&lt;li&gt;exists(): 필드 존재 여부&lt;/li&gt;
&lt;li&gt;lean(): 순수 JavaScript 객체 반환 (성능 향상)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Population&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Population은 다른 컬렉션의 문서를 참조하여 자동으로 채워주는 기능입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 스키마 정의
const authorSchema = new Schema({
  name: String,
  email: String
});

const postSchema = new Schema({
  title: String,
  content: String,
  author: {
    type: Schema.Types.ObjectId,
    ref: 'Author'
  },
  comments: [{
    type: Schema.Types.ObjectId,
    ref: 'Comment'
  }]
});

const Author = mongoose.model('Author', authorSchema);
const Post = mongoose.model('Post', postSchema);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// Population 사용
const post = await Post.findById(postId)
  .populate('author')
  .populate({
    path: 'comments',
    select: 'text createdAt',
    options: { sort: { createdAt: -1 }, limit: 5 }
  });

// 중첩 Population
const post = await Post.findById(postId)
  .populate({
    path: 'comments',
    populate: {
      path: 'author',
      select: 'name'
    }
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 가상 속성(Virtuals)과 인스턴스 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가상 속성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 속성은 데이터베이스에 저장되지 않지만, 문서에서 접근할 수 있는 속성입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const userSchema = new Schema({
  firstName: String,
  lastName: String,
  birthYear: Number
});

// getter 가상 속성
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// setter 가상 속성
userSchema.virtual('fullName').set(function(name) {
  const [firstName, lastName] = name.split(' ');
  this.firstName = firstName;
  this.lastName = lastName;
});

// 계산된 속성
userSchema.virtual('age').get(function() {
  return new Date().getFullYear() - this.birthYear;
});

// JSON/Object 변환 시 가상 속성 포함
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const user = new User({
  firstName: '길동',
  lastName: '홍',
  birthYear: 1990
});

console.log(user.fullName); // '길동 홍'
console.log(user.age);      // 계산된 나이&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인스턴스 메서드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 메서드는 개별 문서에서 호출할 수 있는 메서드입니다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;userSchema.methods.comparePassword = async function(candidatePassword) {
  const bcrypt = require('bcrypt');
  return bcrypt.compare(candidatePassword, this.password);
};

userSchema.methods.generateToken = function() {
  const jwt = require('jsonwebtoken');
  return jwt.sign(
    { id: this._id, email: this.email },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
};

userSchema.methods.toPublicJSON = function() {
  return {
    id: this._id,
    name: this.name,
    email: this.email,
    createdAt: this.createdAt
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user = await User.findOne({ email: 'hong@example.com' });
const isMatch = await user.comparePassword('password123');
const token = user.generateToken();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정적 메서드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 메서드는 모델 자체에서 호출하는 메서드입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

userSchema.statics.findActiveUsers = function() {
  return this.find({ isActive: true, isDeleted: false });
};

userSchema.statics.getStatistics = async function() {
  return this.aggregate([
    { $group: { _id: '$role', count: { $sum: 1 } } }
  ]);
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user = await User.findByEmail('Hong@Example.com');
const activeUsers = await User.findActiveUsers();
const stats = await User.getStatistics();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mongoose는 MongoDB와 Node.js 애플리케이션을 연결하는 강력한 ODM 라이브러리입니다. 스키마 정의를 통해 데이터 구조를 명확히 하고, 유효성 검사로 데이터 무결성을 보장하며, 미들웨어와 가상 속성으로 비즈니스 로직을 효과적으로 구현할 수 있습니다. Population 기능은 관계형 데이터베이스의 조인과 유사한 기능을 제공하여 문서 간 참조를 쉽게 처리할 수 있게 해줍니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/823</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Mongoose-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry823comment</comments>
      <pubDate>Tue, 17 Mar 2026 20:00:15 +0900</pubDate>
    </item>
    <item>
      <title>Node.js MongoDB 연동</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-MongoDB-%EC%97%B0%EB%8F%99</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션에서 MongoDB를 연동하는 방법을 알아봅니다. MongoDB 네이티브 드라이버를 사용하여 데이터베이스에 연결하고 CRUD 작업을 수행하는 전체 과정을 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. MongoDB 소개와 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB는 문서 지향 NoSQL 데이터베이스입니다. 데이터를 JSON과 유사한 BSON(Binary JSON) 형식으로 저장하며, 스키마가 유연하여 다양한 형태의 데이터를 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문서 기반 저장&lt;/b&gt;: 데이터를 컬렉션 내의 문서로 저장하며, 각 문서는 고유한 &lt;code&gt;_id&lt;/code&gt; 필드를 가집니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 스키마&lt;/b&gt;: 같은 컬렉션 내에서도 문서마다 다른 필드를 가질 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수평적 확장&lt;/b&gt;: 샤딩을 통해 여러 서버에 데이터를 분산 저장할 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풍부한 쿼리 언어&lt;/b&gt;: 필터링, 정렬, 집계 등 다양한 쿼리 기능을 제공합니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱싱 지원&lt;/b&gt;: 단일 필드, 복합 필드, 텍스트 인덱스 등을 지원합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. mongodb 네이티브 드라이버 설치 및 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 MongoDB를 사용하려면 공식 MongoDB Node.js 드라이버를 설치해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 패키지 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install mongodb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 기본 설정&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { MongoClient } = require('mongodb');

// MongoDB 연결 URI
const uri = 'mongodb://localhost:27017';

// 클라이언트 인스턴스 생성
const client = new MongoClient(uri);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 프로젝트 구조 예시&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;project/
├── package.json
├── src/
│   ├── db/
│   │   └── connection.js
│   ├── models/
│   │   └── user.js
│   └── index.js
└── .env&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 연결 문자열(Connection String) 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB 연결 문자열은 데이터베이스 연결에 필요한 모든 정보를 포함합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 로컬 MongoDB 연결&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// 기본 로컬 연결
const uri = 'mongodb://localhost:27017';

// 특정 데이터베이스 지정
const uri = 'mongodb://localhost:27017/myDatabase';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 인증이 필요한 연결&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// 사용자명과 비밀번호 포함
const uri = 'mongodb://username:password@localhost:27017/myDatabase';

// 인증 데이터베이스 지정
const uri = 'mongodb://username:password@localhost:27017/myDatabase?authSource=admin';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 MongoDB Atlas 클라우드 연결&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// MongoDB Atlas 연결 문자열
const uri = 'mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/myDatabase?retryWrites=true&amp;amp;w=majority';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 연결 옵션&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';

const client = new MongoClient(uri, {
  maxPoolSize: 10,           // 연결 풀 최대 크기
  minPoolSize: 5,            // 연결 풀 최소 크기
  serverSelectionTimeoutMS: 5000,  // 서버 선택 타임아웃
  socketTimeoutMS: 45000,    // 소켓 타임아웃
  connectTimeoutMS: 10000,   // 연결 타임아웃
  retryWrites: true,         // 쓰기 작업 재시도
  w: 'majority'              // 쓰기 확인 수준
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 데이터베이스 연결 및 연결 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 기본 연결&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function connect() {
  try {
    await client.connect();
    console.log('MongoDB에 연결되었습니다.');

    // 데이터베이스 선택
    const db = client.db('myDatabase');

    // 컬렉션 선택
    const collection = db.collection('users');

    return { db, collection };
  } catch (error) {
    console.error('연결 오류:', error);
    throw error;
  }
}

connect();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 연결 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function checkConnection() {
  try {
    await client.connect();

    // ping 명령으로 연결 확인
    await client.db('admin').command({ ping: 1 });
    console.log('MongoDB 연결이 정상입니다.');

    return true;
  } catch (error) {
    console.error('연결 확인 실패:', error);
    return false;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 싱글톤 패턴으로 연결 관리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// db/connection.js
const { MongoClient } = require('mongodb');

const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';

let client = null;
let db = null;

async function connectDB(dbName = 'myDatabase') {
  if (db) {
    return db;
  }

  try {
    client = new MongoClient(uri);
    await client.connect();
    db = client.db(dbName);
    console.log(`${dbName} 데이터베이스에 연결되었습니다.`);
    return db;
  } catch (error) {
    console.error('데이터베이스 연결 실패:', error);
    throw error;
  }
}

function getDB() {
  if (!db) {
    throw new Error('데이터베이스가 연결되지 않았습니다. connectDB()를 먼저 호출하세요.');
  }
  return db;
}

async function closeDB() {
  if (client) {
    await client.close();
    client = null;
    db = null;
    console.log('데이터베이스 연결이 종료되었습니다.');
  }
}

module.exports = { connectDB, getDB, closeDB };&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 연결 관리 모듈 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// index.js
const { connectDB, getDB, closeDB } = require('./db/connection');

async function main() {
  // 데이터베이스 연결
  await connectDB('myDatabase');

  // 연결된 DB 인스턴스 사용
  const db = getDB();
  const users = db.collection('users');

  // 작업 수행
  const result = await users.find({}).toArray();
  console.log(result);

  // 연결 종료
  await closeDB();
}

main().catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CRUD 작업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 문서 삽입 (Create)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 문서 삽입 - insertOne&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function insertOneDocument(collection) {
  const document = {
    name: '홍길동',
    email: 'hong@example.com',
    age: 30,
    createdAt: new Date()
  };

  const result = await collection.insertOne(document);

  console.log('삽입된 문서 ID:', result.insertedId);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 문서 삽입 - insertMany&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;async function insertManyDocuments(collection) {
  const documents = [
    { name: '김철수', email: 'kim@example.com', age: 25 },
    { name: '이영희', email: 'lee@example.com', age: 28 },
    { name: '박민수', email: 'park@example.com', age: 35 }
  ];

  const result = await collection.insertMany(documents);

  console.log('삽입된 문서 수:', result.insertedCount);
  console.log('삽입된 문서 IDs:', result.insertedIds);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 문서 조회 (Read)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 문서 조회 - findOne&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function findOneDocument(collection) {
  // 조건에 맞는 첫 번째 문서 조회
  const document = await collection.findOne({ name: '홍길동' });

  console.log('조회된 문서:', document);
  return document;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 문서 조회 - find&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function findDocuments(collection) {
  // 모든 문서 조회
  const allDocs = await collection.find({}).toArray();

  // 조건에 맞는 문서 조회
  const filteredDocs = await collection.find({ age: { $gte: 30 } }).toArray();

  // 정렬과 제한
  const sortedDocs = await collection
    .find({})
    .sort({ age: -1 })  // 나이 내림차순
    .limit(5)           // 5개만 조회
    .toArray();

  // 특정 필드만 조회 (프로젝션)
  const projectedDocs = await collection
    .find({}, { projection: { name: 1, email: 1, _id: 0 } })
    .toArray();

  return { allDocs, filteredDocs, sortedDocs, projectedDocs };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 연산자 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;async function queryWithOperators(collection) {
  // 비교 연산자
  const gteDocs = await collection.find({ age: { $gte: 25 } }).toArray();    // 25 이상
  const ltDocs = await collection.find({ age: { $lt: 30 } }).toArray();      // 30 미만
  const neDocs = await collection.find({ status: { $ne: 'inactive' } }).toArray(); // 같지 않음

  // 논리 연산자
  const andDocs = await collection.find({
    $and: [
      { age: { $gte: 25 } },
      { status: 'active' }
    ]
  }).toArray();

  const orDocs = await collection.find({
    $or: [
      { age: { $lt: 25 } },
      { age: { $gt: 35 } }
    ]
  }).toArray();

  // 배열 연산자
  const inDocs = await collection.find({
    status: { $in: ['active', 'pending'] }
  }).toArray();

  return { gteDocs, ltDocs, andDocs, orDocs, inDocs };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 문서 수정 (Update)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 문서 수정 - updateOne&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function updateOneDocument(collection) {
  const filter = { name: '홍길동' };
  const update = {
    $set: { age: 31, updatedAt: new Date() },
    $inc: { loginCount: 1 }
  };

  const result = await collection.updateOne(filter, update);

  console.log('매칭된 문서 수:', result.matchedCount);
  console.log('수정된 문서 수:', result.modifiedCount);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 문서 수정 - updateMany&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function updateManyDocuments(collection) {
  const filter = { status: 'inactive' };
  const update = {
    $set: { status: 'active', activatedAt: new Date() }
  };

  const result = await collection.updateMany(filter, update);

  console.log('매칭된 문서 수:', result.matchedCount);
  console.log('수정된 문서 수:', result.modifiedCount);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서 교체 - replaceOne&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function replaceDocument(collection) {
  const filter = { name: '홍길동' };
  const replacement = {
    name: '홍길동',
    email: 'newhong@example.com',
    age: 32,
    profile: {
      bio: '개발자',
      website: 'https://example.com'
    },
    updatedAt: new Date()
  };

  const result = await collection.replaceOne(filter, replacement);

  console.log('교체된 문서 수:', result.modifiedCount);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;upsert 옵션 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function upsertDocument(collection) {
  const filter = { email: 'new@example.com' };
  const update = {
    $set: { name: '신규사용자', age: 20 },
    $setOnInsert: { createdAt: new Date() }
  };
  const options = { upsert: true };

  const result = await collection.updateOne(filter, update, options);

  console.log('upserted ID:', result.upsertedId);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 문서 삭제 (Delete)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 문서 삭제 - deleteOne&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;async function deleteOneDocument(collection) {
  const filter = { name: '홍길동' };

  const result = await collection.deleteOne(filter);

  console.log('삭제된 문서 수:', result.deletedCount);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 문서 삭제 - deleteMany&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function deleteManyDocuments(collection) {
  // 조건에 맞는 여러 문서 삭제
  const filter = { status: 'inactive' };

  const result = await collection.deleteMany(filter);

  console.log('삭제된 문서 수:', result.deletedCount);
  return result;
}

async function deleteAllDocuments(collection) {
  // 모든 문서 삭제 (주의: 컬렉션의 모든 데이터 삭제)
  const result = await collection.deleteMany({});

  console.log('삭제된 문서 수:', result.deletedCount);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 인덱스 생성 및 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 쿼리 성능을 크게 향상시킵니다. 자주 검색하는 필드에 인덱스를 생성하면 조회 속도가 빨라집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 단일 필드 인덱스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function createSingleIndex(collection) {
  // 오름차순 인덱스 생성
  const result = await collection.createIndex({ email: 1 });

  console.log('생성된 인덱스:', result);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 복합 인덱스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function createCompoundIndex(collection) {
  // 여러 필드를 조합한 인덱스
  const result = await collection.createIndex(
    { status: 1, createdAt: -1 },
    { name: 'status_createdAt_idx' }
  );

  console.log('생성된 복합 인덱스:', result);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 고유 인덱스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function createUniqueIndex(collection) {
  // 중복을 허용하지 않는 인덱스
  const result = await collection.createIndex(
    { email: 1 },
    { unique: true }
  );

  console.log('생성된 고유 인덱스:', result);
  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 텍스트 인덱스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function createTextIndex(collection) {
  // 텍스트 검색을 위한 인덱스
  const result = await collection.createIndex(
    { title: 'text', content: 'text' },
    { default_language: 'korean' }
  );

  console.log('생성된 텍스트 인덱스:', result);
  return result;
}

// 텍스트 검색 사용
async function textSearch(collection) {
  const results = await collection.find({
    $text: { $search: '검색어' }
  }).toArray();

  return results;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.5 인덱스 조회 및 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function manageIndexes(collection) {
  // 모든 인덱스 조회
  const indexes = await collection.indexes();
  console.log('현재 인덱스 목록:', indexes);

  // 특정 인덱스 삭제
  await collection.dropIndex('email_1');

  // 모든 인덱스 삭제 (_id 인덱스 제외)
  await collection.dropIndexes();

  return indexes;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 에러 처리 및 연결 종료&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const { MongoClient, MongoServerError } = require('mongodb');

async function handleErrors(collection) {
  try {
    // 중복 키 삽입 시도
    await collection.insertOne({ _id: 'duplicate', name: '테스트' });
    await collection.insertOne({ _id: 'duplicate', name: '테스트2' });
  } catch (error) {
    if (error instanceof MongoServerError) {
      if (error.code === 11000) {
        console.error('중복 키 오류:', error.message);
      } else {
        console.error('MongoDB 서버 오류:', error.message);
      }
    } else {
      console.error('예상치 못한 오류:', error);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 연결 오류 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function connectWithRetry(uri, maxRetries = 3) {
  const client = new MongoClient(uri);

  for (let attempt = 1; attempt &amp;lt;= maxRetries; attempt++) {
    try {
      await client.connect();
      console.log('MongoDB 연결 성공');
      return client;
    } catch (error) {
      console.error(`연결 시도 ${attempt}/${maxRetries} 실패:`, error.message);

      if (attempt === maxRetries) {
        throw new Error('최대 재시도 횟수 초과');
      }

      // 재시도 전 대기
      await new Promise(resolve =&amp;gt; setTimeout(resolve, 1000 * attempt));
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 연결 종료 처리&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;async function gracefulShutdown(client) {
  process.on('SIGINT', async () =&amp;gt; {
    console.log('애플리케이션 종료 중...');

    try {
      await client.close();
      console.log('MongoDB 연결이 정상적으로 종료되었습니다.');
      process.exit(0);
    } catch (error) {
      console.error('연결 종료 중 오류:', error);
      process.exit(1);
    }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 전체 예제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';

async function main() {
  const client = new MongoClient(uri);

  try {
    // 연결
    await client.connect();
    console.log('MongoDB에 연결되었습니다.');

    const db = client.db('testDB');
    const users = db.collection('users');

    // 문서 삽입
    const insertResult = await users.insertOne({
      name: '테스트 사용자',
      email: 'test@example.com',
      createdAt: new Date()
    });
    console.log('삽입된 문서 ID:', insertResult.insertedId);

    // 문서 조회
    const user = await users.findOne({ email: 'test@example.com' });
    console.log('조회된 사용자:', user);

    // 문서 수정
    const updateResult = await users.updateOne(
      { email: 'test@example.com' },
      { $set: { name: '수정된 이름' } }
    );
    console.log('수정된 문서 수:', updateResult.modifiedCount);

    // 문서 삭제
    const deleteResult = await users.deleteOne({ email: 'test@example.com' });
    console.log('삭제된 문서 수:', deleteResult.deletedCount);

  } catch (error) {
    console.error('오류 발생:', error);
  } finally {
    // 연결 종료
    await client.close();
    console.log('MongoDB 연결이 종료되었습니다.');
  }
}

main();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 MongoDB 네이티브 드라이버를 사용하면 문서 기반 데이터베이스의 유연성을 그대로 활용할 수 있습니다. 연결 풀 관리, 적절한 인덱스 설정, 에러 처리를 통해 안정적인 데이터베이스 연동을 구현할 수 있으며, 비동기 처리 패턴과 함께 사용하면 효율적인 애플리케이션을 개발할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/822</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-MongoDB-%EC%97%B0%EB%8F%99#entry822comment</comments>
      <pubDate>Tue, 17 Mar 2026 08:00:29 +0900</pubDate>
    </item>
    <item>
      <title>Node.js 데이터베이스 연동(Database Integration) 완벽 가이드</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99Database-Integration-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h1&gt;Node.js 데이터베이스 연동(Database Integration) 완벽 가이드&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 애플리케이션에서 데이터베이스 연동은 백엔드 개발의 핵심 영역이다. 이 글에서는 Node.js에서 다양한 데이터베이스를 연동하는 방법과 핵심 개념들을 실용적인 관점에서 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Node.js에서 데이터베이스 연동의 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 비동기 I/O 기반의 런타임으로, 데이터베이스 작업과 같은 I/O 집약적 작업에 최적화되어 있다. 데이터베이스 연동이 중요한 이유는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 영속성 확보&lt;/b&gt;: 애플리케이션 재시작 후에도 데이터가 유지되어야 한다. 메모리에 저장된 데이터는 프로세스 종료 시 사라지므로, 데이터베이스를 통한 영속성 확보가 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확장성 있는 아키텍처&lt;/b&gt;: 여러 Node.js 인스턴스가 동일한 데이터베이스에 접근하여 데이터를 공유할 수 있다. 이는 수평적 확장(horizontal scaling)의 기본 조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;효율적인 데이터 관리&lt;/b&gt;: 데이터베이스는 인덱싱, 쿼리 최적화, 트랜잭션 처리 등 데이터 관리에 필요한 기능을 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 지원되는 데이터베이스 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 SQL과 NoSQL 모든 종류의 데이터베이스를 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 데이터베이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스는 정형화된 스키마와 ACID 특성을 제공한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;데이터베이스&lt;/th&gt;
&lt;th&gt;주요 드라이버&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;mysql2&lt;/td&gt;
&lt;td&gt;가장 널리 사용되는 오픈소스 RDBMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;pg&lt;/td&gt;
&lt;td&gt;고급 기능과 확장성, JSON 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;better-sqlite3&lt;/td&gt;
&lt;td&gt;파일 기반 경량 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MariaDB&lt;/td&gt;
&lt;td&gt;mariadb&lt;/td&gt;
&lt;td&gt;MySQL 포크, 성능 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft SQL Server&lt;/td&gt;
&lt;td&gt;mssql&lt;/td&gt;
&lt;td&gt;엔터프라이즈 환경에서 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NoSQL 데이터베이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관계형 데이터베이스는 유연한 스키마와 수평적 확장성을 제공한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;데이터베이스&lt;/th&gt;
&lt;th&gt;주요 드라이버&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;mongodb, mongoose&lt;/td&gt;
&lt;td&gt;문서 지향 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;ioredis, redis&lt;/td&gt;
&lt;td&gt;인메모리 키-값 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;cassandra-driver&lt;/td&gt;
&lt;td&gt;분산형 와이드 컬럼 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;@aws-sdk/client-dynamodb&lt;/td&gt;
&lt;td&gt;AWS 관리형 NoSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;@elastic/elasticsearch&lt;/td&gt;
&lt;td&gt;검색 엔진 및 분석 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터베이스 드라이버와 ORM/ODM 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네이티브 드라이버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 드라이버는 Node.js와 데이터베이스 간의 저수준 통신을 담당한다. SQL 쿼리를 직접 작성하여 데이터베이스와 통신한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// MySQL 네이티브 드라이버 예제 (mysql2)
const mysql = require('mysql2/promise');

async function queryWithDriver() {
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'myapp'
  });

  const [rows] = await connection.execute(
    'SELECT * FROM users WHERE id = ?',
    [1]
  );

  await connection.end();
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ORM (Object-Relational Mapping)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM은 객체와 관계형 데이터베이스 테이블을 매핑하여 SQL을 직접 작성하지 않고도 데이터베이스를 조작할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 Node.js ORM&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sequelize: MySQL, PostgreSQL, SQLite, MariaDB 지원&lt;/li&gt;
&lt;li&gt;TypeORM: TypeScript 기반, 다양한 데이터베이스 지원&lt;/li&gt;
&lt;li&gt;Prisma: 타입 안전성과 자동 생성 쿼리 제공&lt;/li&gt;
&lt;li&gt;Knex.js: SQL 쿼리 빌더 (엄밀히는 ORM이 아닌 쿼리 빌더)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// Sequelize ORM 예제
const { Sequelize, DataTypes } = require('sequelize');

const sequelize = new Sequelize('mysql://user:pass@localhost:3306/myapp');

const User = sequelize.define('User', {
  username: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  }
});

// 사용 예
async function createUser() {
  const user = await User.create({
    username: 'john_doe',
    email: 'john@example.com'
  });
  return user;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ODM (Object-Document Mapping)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ODM은 NoSQL 문서 데이터베이스에서 ORM과 유사한 역할을 한다. MongoDB의 경우 Mongoose가 대표적이다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// Mongoose ODM 예제
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  createdAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);

async function findUsers() {
  await mongoose.connect('mongodb://localhost:27017/myapp');
  const users = await User.find({ username: /^john/i });
  return users;
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 연결 풀링(Connection Pooling) 개념과 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 풀링은 데이터베이스 연결을 미리 생성하여 풀에 보관하고, 요청 시 재사용하는 기법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 풀링이 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연결 생성 비용 절감&lt;/b&gt;: 데이터베이스 연결 수립에는 TCP 핸드셰이크, 인증, 세션 초기화 등 비용이 발생한다. 풀링을 통해 이 비용을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동시 요청 처리&lt;/b&gt;: 여러 요청이 동시에 들어올 때 각각 새 연결을 만들지 않고 풀의 연결을 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리소스 관리&lt;/b&gt;: 데이터베이스 서버의 최대 연결 수 제한을 효율적으로 관리할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 풀 구현 예제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// mysql2 연결 풀 예제
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'myapp',
  waitForConnections: true,
  connectionLimit: 10,      // 풀의 최대 연결 수
  queueLimit: 0,            // 대기 큐 제한 (0은 무제한)
  idleTimeout: 60000,       // 유휴 연결 타임아웃 (ms)
  enableKeepAlive: true,
  keepAliveInitialDelay: 0
});

// 풀에서 연결 획득 및 반환
async function queryWithPool() {
  const connection = await pool.getConnection();
  try {
    const [rows] = await connection.execute('SELECT * FROM users');
    return rows;
  } finally {
    connection.release(); // 풀에 연결 반환
  }
}

// 또는 풀 직접 사용 (자동 연결 관리)
async function simpleQuery() {
  const [rows] = await pool.execute('SELECT * FROM users WHERE active = ?', [true]);
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연결 풀 설정 권장값&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;권장값&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;connectionLimit&lt;/td&gt;
&lt;td&gt;CPU 코어 수 * 2 ~ 4&lt;/td&gt;
&lt;td&gt;동시 연결 최대 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;waitForConnections&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;풀이 가득 찰 때 대기 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queueLimit&lt;/td&gt;
&lt;td&gt;0 또는 적절한 값&lt;/td&gt;
&lt;td&gt;대기 큐 크기 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idleTimeout&lt;/td&gt;
&lt;td&gt;60000 (60초)&lt;/td&gt;
&lt;td&gt;유휴 연결 유지 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 비동기 쿼리 처리 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 비동기 특성을 활용한 데이터베이스 쿼리 처리 패턴을 살펴본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async/await 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 권장되는 현대적인 비동기 처리 방식이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getUserWithPosts(userId) {
  const user = await User.findByPk(userId);
  if (!user) {
    throw new Error('User not found');
  }

  const posts = await Post.findAll({
    where: { userId: user.id }
  });

  return { user, posts };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병렬 쿼리 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;독립적인 쿼리는 병렬로 실행하여 성능을 향상시킨다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async function getDashboardData(userId) {
  // 병렬 실행으로 전체 응답 시간 단축
  const [user, orders, notifications] = await Promise.all([
    User.findByPk(userId),
    Order.findAll({ where: { userId }, limit: 10 }),
    Notification.findAll({ where: { userId, read: false } })
  ]);

  return { user, orders, notifications };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순차 쿼리 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 쿼리 결과에 의존하는 경우 순차 실행이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function processOrder(orderId) {
  const order = await Order.findByPk(orderId);

  // order 결과를 사용하여 다음 쿼리 실행
  const items = await OrderItem.findAll({
    where: { orderId: order.id }
  });

  // items 결과를 사용
  const total = items.reduce((sum, item) =&amp;gt; sum + item.price * item.quantity, 0);

  await order.update({ total, status: 'processed' });

  return order;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스트림 기반 대용량 데이터 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 데이터를 처리할 때는 스트림을 활용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { pipeline } = require('stream/promises');
const { Transform } = require('stream');

async function exportLargeDataset() {
  const connection = await pool.getConnection();

  try {
    const stream = connection.query('SELECT * FROM large_table')
      .stream();

    const transform = new Transform({
      objectMode: true,
      transform(row, encoding, callback) {
        // 각 행을 JSON 문자열로 변환
        this.push(JSON.stringify(row) + '\n');
        callback();
      }
    });

    await pipeline(stream, transform, fs.createWriteStream('export.json'));
  } finally {
    connection.release();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 에러 처리 및 트랜잭션 기본 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 에러 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 작업에서 발생할 수 있는 에러를 적절히 처리해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;async function safeQuery(userId) {
  try {
    const user = await User.findByPk(userId);
    return user;
  } catch (error) {
    if (error.code === 'ECONNREFUSED') {
      console.error('Database connection failed');
      throw new Error('Service temporarily unavailable');
    }

    if (error.code === 'ER_DUP_ENTRY') {
      throw new Error('Duplicate entry exists');
    }

    if (error.name === 'SequelizeValidationError') {
      throw new Error(`Validation failed: ${error.message}`);
    }

    console.error('Unexpected database error:', error);
    throw error;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시도 로직 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일시적인 연결 문제에 대응하는 재시도 로직이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function queryWithRetry(queryFn, maxRetries = 3, delay = 1000) {
  for (let attempt = 1; attempt &amp;lt;= maxRetries; attempt++) {
    try {
      return await queryFn();
    } catch (error) {
      const isRetryable = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET']
        .includes(error.code);

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise(resolve =&amp;gt; setTimeout(resolve, delay * attempt));
    }
  }
}

// 사용 예
const result = await queryWithRetry(() =&amp;gt; User.findAll());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 여러 데이터베이스 작업을 하나의 원자적 단위로 묶어 실행한다. 모든 작업이 성공하면 커밋되고, 하나라도 실패하면 전체가 롤백된다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// Sequelize 트랜잭션 예제
async function transferFunds(fromUserId, toUserId, amount) {
  const transaction = await sequelize.transaction();

  try {
    // 출금
    const fromAccount = await Account.findOne({
      where: { userId: fromUserId },
      transaction,
      lock: transaction.LOCK.UPDATE // 비관적 잠금
    });

    if (fromAccount.balance &amp;lt; amount) {
      throw new Error('Insufficient funds');
    }

    await fromAccount.decrement('balance', { by: amount, transaction });

    // 입금
    const toAccount = await Account.findOne({
      where: { userId: toUserId },
      transaction
    });

    await toAccount.increment('balance', { by: amount, transaction });

    // 거래 기록
    await Transaction.create({
      fromUserId,
      toUserId,
      amount,
      type: 'transfer'
    }, { transaction });

    await transaction.commit();
    return { success: true };

  } catch (error) {
    await transaction.rollback();
    throw error;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관리 트랜잭션 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sequelize의 관리 트랜잭션을 사용하면 자동으로 커밋/롤백이 처리된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function managedTransaction() {
  const result = await sequelize.transaction(async (t) =&amp;gt; {
    const user = await User.create({
      username: 'newuser',
      email: 'new@example.com'
    }, { transaction: t });

    await Profile.create({
      userId: user.id,
      bio: 'Hello!'
    }, { transaction: t });

    return user;
    // 정상 완료 시 자동 커밋, 에러 발생 시 자동 롤백
  });

  return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 데이터베이스 선택 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 특성에 따라 적합한 데이터베이스를 선택해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 데이터베이스 선택 시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 쿼리와 조인이 많은 경우&lt;/li&gt;
&lt;li&gt;JSON 데이터와 관계형 데이터를 함께 다루는 경우&lt;/li&gt;
&lt;li&gt;지리공간 데이터 처리 (PostGIS)&lt;/li&gt;
&lt;li&gt;ACID 준수가 중요한 금융, 결제 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 작업이 많은 웹 애플리케이션&lt;/li&gt;
&lt;li&gt;기존 MySQL 기반 시스템과의 호환성&lt;/li&gt;
&lt;li&gt;풍부한 호스팅 옵션 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQLite 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 사용자 애플리케이션&lt;/li&gt;
&lt;li&gt;프로토타입 및 개발 환경&lt;/li&gt;
&lt;li&gt;임베디드 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NoSQL 데이터베이스 선택 시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MongoDB 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스키마가 자주 변경되는 경우&lt;/li&gt;
&lt;li&gt;문서 형태의 비정형 데이터&lt;/li&gt;
&lt;li&gt;빠른 개발 속도가 중요한 경우&lt;/li&gt;
&lt;li&gt;수평적 확장이 필요한 대규모 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 저장소&lt;/li&gt;
&lt;li&gt;캐싱 레이어&lt;/li&gt;
&lt;li&gt;실시간 리더보드, 카운터&lt;/li&gt;
&lt;li&gt;메시지 큐, Pub/Sub&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Elasticsearch 선택 권장 상황&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전문 검색(Full-text search) 기능&lt;/li&gt;
&lt;li&gt;로그 분석 및 모니터링&lt;/li&gt;
&lt;li&gt;대규모 텍스트 데이터 검색&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;혼합 사용 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로덕션 환경에서는 여러 데이터베이스를 조합하여 사용하는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 복합 데이터 저장소 아키텍처 예시
const architecture = {
  primaryData: 'PostgreSQL',    // 핵심 비즈니스 데이터
  cache: 'Redis',               // 캐싱 및 세션
  search: 'Elasticsearch',      // 검색 기능
  analytics: 'ClickHouse'       // 분석용 데이터
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 데이터베이스 연동은 연결 풀링을 통한 효율적인 연결 관리, 비동기 패턴을 활용한 쿼리 처리, 트랜잭션을 통한 데이터 무결성 보장이 핵심이다. 프로젝트 요구사항에 맞는 데이터베이스를 선택하고, ORM/ODM 사용 여부를 결정하여 생산성과 성능의 균형을 맞추는 것이 중요하다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/821</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%97%B0%EB%8F%99Database-Integration-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry821comment</comments>
      <pubDate>Mon, 16 Mar 2026 20:00:57 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 쿠키 관리(Cookie Management)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BF%A0%ED%82%A4-%EA%B4%80%EB%A6%ACCookie-Management</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 쿠키의 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 웹 서버가 사용자의 브라우저에 저장하는 작은 텍스트 데이터입니다. HTTP는 무상태(stateless) 프로토콜이므로, 쿠키를 통해 사용자를 식별하고 상태를 유지합니다. 쿠키는 로그인 상태 유지, 사용자 설정 저장, 방문 기록 추적 등에 활용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 HTTP 쿠키 동작 원리&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;1. 클라이언트 &amp;rarr; 서버: 최초 요청

2. 서버 &amp;rarr; 클라이언트: 응답 헤더에 Set-Cookie 포함
   Set-Cookie: sessionId=abc123; HttpOnly; Secure

3. 클라이언트: 쿠키를 브라우저에 저장

4. 클라이언트 &amp;rarr; 서버: 이후 요청마다 Cookie 헤더에 자동 포함
   Cookie: sessionId=abc123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 응답 시 Set-Cookie 헤더로 쿠키를 설정하고, 브라우저는 이후 같은 도메인으로 요청할 때 Cookie 헤더에 저장된 쿠키를 자동으로 포함합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. cookie-parser 미들웨어 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express.js에서 쿠키를 다루려면 cookie-parser 미들웨어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install cookie-parser&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 설정&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();

// cookie-parser 미들웨어 등록
app.use(cookieParser());

app.get('/', (req, res) =&amp;gt; {
  // req.cookies로 쿠키 접근
  console.log(req.cookies);
  res.send('쿠키 확인');
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cookie-parser를 등록하면 req.cookies 객체를 통해 클라이언트가 보낸 쿠키에 접근할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 서명된 쿠키를 위한 설정&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 비밀 키를 전달하면 서명된 쿠키 사용 가능
app.use(cookieParser('my-secret-key'));

// 서명된 쿠키는 req.signedCookies로 접근
app.get('/', (req, res) =&amp;gt; {
  console.log(req.cookies);        // 일반 쿠키
  console.log(req.signedCookies);  // 서명된 쿠키
  res.send('확인');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 쿠키 설정 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;res.cookie() 메서드로 쿠키를 설정할 때 다양한 옵션을 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;res.cookie('name', 'value', {
  httpOnly: true,         // JavaScript에서 접근 불가 (XSS 방지)
  secure: true,           // HTTPS에서만 쿠키 전송
  maxAge: 1000 * 60 * 60, // 밀리초 단위 (1시간)
  expires: new Date(),    // 만료 날짜 (Date 객체)
  domain: 'example.com',  // 쿠키가 유효한 도메인
  path: '/',              // 쿠키가 유효한 경로
  sameSite: 'strict',     // CSRF 방지 설정
  signed: true            // 서명된 쿠키 사용
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 옵션 상세 설명&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;권장값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;httpOnly&lt;/td&gt;
&lt;td&gt;true면 클라이언트 JavaScript에서 접근 불가&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;secure&lt;/td&gt;
&lt;td&gt;true면 HTTPS에서만 쿠키 전송&lt;/td&gt;
&lt;td&gt;프로덕션: true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxAge&lt;/td&gt;
&lt;td&gt;쿠키 유효 기간 (밀리초)&lt;/td&gt;
&lt;td&gt;용도에 따라 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expires&lt;/td&gt;
&lt;td&gt;쿠키 만료 시점 (Date 객체)&lt;/td&gt;
&lt;td&gt;maxAge 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;domain&lt;/td&gt;
&lt;td&gt;쿠키가 전송되는 도메인&lt;/td&gt;
&lt;td&gt;기본값 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;path&lt;/td&gt;
&lt;td&gt;쿠키가 전송되는 경로&lt;/td&gt;
&lt;td&gt;'/'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sameSite&lt;/td&gt;
&lt;td&gt;CSRF 방지 (strict, lax, none)&lt;/td&gt;
&lt;td&gt;strict 또는 lax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;signed&lt;/td&gt;
&lt;td&gt;서명 여부&lt;/td&gt;
&lt;td&gt;민감 데이터: true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 sameSite 옵션&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// strict: 같은 사이트 요청에서만 쿠키 전송
res.cookie('session', 'abc', { sameSite: 'strict' });

// lax: GET 요청으로 외부 링크 클릭 시에도 쿠키 전송 (기본값)
res.cookie('session', 'abc', { sameSite: 'lax' });

// none: 크로스 사이트 요청에도 쿠키 전송 (secure: true 필수)
res.cookie('session', 'abc', { sameSite: 'none', secure: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 서명된 쿠키(Signed Cookie)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서명된 쿠키는 쿠키 값의 무결성을 검증할 수 있습니다. 클라이언트가 쿠키 값을 임의로 변경했는지 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 서명된 쿠키 설정 및 읽기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();

// 비밀 키로 cookie-parser 초기화
app.use(cookieParser('my-super-secret-key'));

// 서명된 쿠키 설정
app.get('/set-signed', (req, res) =&amp;gt; {
  res.cookie('userId', '12345', {
    signed: true,
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24  // 1일
  });
  res.json({ message: '서명된 쿠키 설정 완료' });
});

// 서명된 쿠키 읽기
app.get('/get-signed', (req, res) =&amp;gt; {
  const userId = req.signedCookies.userId;

  if (userId) {
    res.json({ userId });
  } else {
    // 서명 검증 실패 또는 쿠키 없음
    res.status(400).json({ error: '유효하지 않은 쿠키' });
  }
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서명된 쿠키는 값이 변조되면 req.signedCookies에서 해당 키가 false로 반환됩니다. 쿠키가 존재하지 않으면 undefined입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 서명 원리&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;설정 시:
value = '12345'
signature = HMAC-SHA256('12345', 'my-super-secret-key')
실제 쿠키 값 = 's:12345.signature'

읽기 시:
1. 쿠키 값에서 원본 데이터와 서명 분리
2. 원본 데이터로 서명 재계산
3. 전달받은 서명과 비교
4. 일치하면 원본 값 반환, 불일치하면 false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 쿠키 CRUD 실제 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 쿠키 설정 (Create)&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;app.get('/set-cookie', (req, res) =&amp;gt; {
  // 기본 쿠키
  res.cookie('username', 'hong');

  // 옵션 포함 쿠키
  res.cookie('preferences', JSON.stringify({ theme: 'dark', lang: 'ko' }), {
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 * 30  // 30일
  });

  // 서명된 쿠키
  res.cookie('token', 'abc123xyz', {
    signed: true,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict'
  });

  res.json({ message: '쿠키 설정 완료' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 쿠키 읽기 (Read)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;app.get('/get-cookie', (req, res) =&amp;gt; {
  // 일반 쿠키 읽기
  const username = req.cookies.username;

  // JSON 쿠키 파싱
  let preferences = null;
  if (req.cookies.preferences) {
    try {
      preferences = JSON.parse(req.cookies.preferences);
    } catch (e) {
      preferences = null;
    }
  }

  // 서명된 쿠키 읽기
  const token = req.signedCookies.token;

  res.json({
    username,
    preferences,
    token: token || null,
    allCookies: req.cookies,
    allSignedCookies: req.signedCookies
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 쿠키 수정 (Update)&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;app.get('/update-cookie', (req, res) =&amp;gt; {
  // 같은 이름으로 다시 설정하면 덮어쓰기
  res.cookie('username', 'kim', {
    httpOnly: true,
    maxAge: 1000 * 60 * 60
  });

  res.json({ message: '쿠키 수정 완료' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 쿠키 삭제 (Delete)&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;app.get('/delete-cookie', (req, res) =&amp;gt; {
  // clearCookie 사용
  res.clearCookie('username');

  // 옵션이 있는 쿠키는 동일한 옵션으로 삭제해야 함
  res.clearCookie('preferences', {
    httpOnly: true,
    path: '/'
  });

  // 서명된 쿠키 삭제
  res.clearCookie('token', {
    signed: true,
    httpOnly: true,
    path: '/'
  });

  res.json({ message: '쿠키 삭제 완료' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키 삭제 시 설정할 때 사용한 path, domain 옵션을 동일하게 지정해야 삭제됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.5 종합 예제&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser('secret-key-123'));
app.use(express.json());

// 방문 횟수 카운터
app.get('/visit', (req, res) =&amp;gt; {
  let visits = parseInt(req.cookies.visits) || 0;
  visits++;

  res.cookie('visits', visits.toString(), {
    maxAge: 1000 * 60 * 60 * 24 * 365,  // 1년
    httpOnly: true
  });

  res.json({ message: `${visits}번째 방문입니다.` });
});

// 사용자 설정 저장
app.post('/settings', (req, res) =&amp;gt; {
  const { theme, fontSize, language } = req.body;

  const settings = { theme, fontSize, language };

  res.cookie('userSettings', JSON.stringify(settings), {
    signed: true,
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 * 30  // 30일
  });

  res.json({ message: '설정 저장 완료', settings });
});

// 사용자 설정 불러오기
app.get('/settings', (req, res) =&amp;gt; {
  const settingsCookie = req.signedCookies.userSettings;

  if (!settingsCookie) {
    return res.json({ settings: null });
  }

  try {
    const settings = JSON.parse(settingsCookie);
    res.json({ settings });
  } catch (e) {
    res.json({ settings: null });
  }
});

// 로그아웃 (관련 쿠키 모두 삭제)
app.post('/logout', (req, res) =&amp;gt; {
  res.clearCookie('visits');
  res.clearCookie('userSettings', { signed: true });

  res.json({ message: '로그아웃 완료' });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 보안 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 필수 보안 설정&lt;/h3&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 프로덕션 환경 쿠키 설정
const cookieOptions = {
  httpOnly: true,                              // XSS 공격 방지
  secure: process.env.NODE_ENV === 'production', // HTTPS 전용
  sameSite: 'strict',                          // CSRF 공격 방지
  maxAge: 1000 * 60 * 15,                      // 15분 (짧게 유지)
  path: '/',
  signed: true                                 // 변조 방지
};

// 민감 정보 쿠키
res.cookie('sessionToken', token, cookieOptions);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 주요 보안 위협과 대응&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위협&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;대응 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;악성 스크립트로 쿠키 탈취&lt;/td&gt;
&lt;td&gt;httpOnly: true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;사용자 권한으로 악의적 요청&lt;/td&gt;
&lt;td&gt;sameSite: strict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중간자 공격&lt;/td&gt;
&lt;td&gt;네트워크에서 쿠키 가로채기&lt;/td&gt;
&lt;td&gt;secure: true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쿠키 변조&lt;/td&gt;
&lt;td&gt;클라이언트가 값 임의 수정&lt;/td&gt;
&lt;td&gt;signed: true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 보안 체크리스트&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const express = require('express');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');

const app = express();

// 보안 헤더 설정
app.use(helmet());

// 프록시 환경 (로드밸런서 뒤)에서 secure 쿠키 사용
app.set('trust proxy', 1);

// 강력한 비밀 키 사용
const COOKIE_SECRET = process.env.COOKIE_SECRET;
if (!COOKIE_SECRET || COOKIE_SECRET.length &amp;lt; 32) {
  throw new Error('COOKIE_SECRET must be at least 32 characters');
}

app.use(cookieParser(COOKIE_SECRET));

// 보안 쿠키 설정 함수
function setSecureCookie(res, name, value, maxAge = 3600000) {
  res.cookie(name, value, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    signed: true,
    maxAge,
    path: '/'
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 민감 정보 저장 금지&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 잘못된 예: 민감 정보를 쿠키에 저장
res.cookie('password', userPassword);  // 절대 금지
res.cookie('creditCard', cardNumber);  // 절대 금지

// 올바른 예: 식별자만 저장하고 서버에서 조회
res.cookie('sessionId', 'random-session-id', {
  httpOnly: true,
  secure: true,
  signed: true
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실무 활용 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 환경별 쿠키 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const cookieConfig = {
  development: {
    httpOnly: true,
    secure: false,
    sameSite: 'lax',
    maxAge: 1000 * 60 * 60 * 24  // 1일
  },
  production: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 1000 * 60 * 60  // 1시간
  }
};

const env = process.env.NODE_ENV || 'development';
const currentConfig = cookieConfig[env];

app.get('/login', (req, res) =&amp;gt; {
  res.cookie('session', sessionId, currentConfig);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 쿠키 유틸리티 함수&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 쿠키 관리 유틸리티
const CookieManager = {
  set(res, name, value, options = {}) {
    const defaultOptions = {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      signed: true,
      path: '/'
    };

    res.cookie(name, value, { ...defaultOptions, ...options });
  },

  get(req, name, signed = true) {
    return signed ? req.signedCookies[name] : req.cookies[name];
  },

  delete(res, name, options = {}) {
    const defaultOptions = {
      path: '/',
      signed: true
    };

    res.clearCookie(name, { ...defaultOptions, ...options });
  },

  setJSON(res, name, data, options = {}) {
    this.set(res, name, JSON.stringify(data), options);
  },

  getJSON(req, name, signed = true) {
    const value = this.get(req, name, signed);
    if (!value) return null;

    try {
      return JSON.parse(value);
    } catch (e) {
      return null;
    }
  }
};

// 사용 예시
app.get('/example', (req, res) =&amp;gt; {
  CookieManager.setJSON(res, 'cart', { items: ['item1', 'item2'] });
  const cart = CookieManager.getJSON(req, 'cart');
  res.json({ cart });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 쿠키 크기 제한&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 쿠키 최대 크기는 약 4KB
// 큰 데이터는 서버에 저장하고 ID만 쿠키에 저장

app.post('/save-data', (req, res) =&amp;gt; {
  const data = req.body;
  const dataSize = JSON.stringify(data).length;

  if (dataSize &amp;gt; 3000) {  // 안전 마진 확보
    // 서버/DB에 저장하고 ID만 쿠키에
    const dataId = saveToDatabase(data);
    res.cookie('dataRef', dataId, { signed: true, httpOnly: true });
  } else {
    // 작은 데이터는 쿠키에 직접 저장
    res.cookie('data', JSON.stringify(data), { signed: true, httpOnly: true });
  }

  res.json({ message: '저장 완료' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 쿠키 동의 처리&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// GDPR 등 규정 준수를 위한 쿠키 동의 확인
app.use((req, res, next) =&amp;gt; {
  const hasConsent = req.cookies.cookieConsent === 'true';
  req.cookieConsent = hasConsent;
  next();
});

app.post('/cookie-consent', (req, res) =&amp;gt; {
  const { accepted } = req.body;

  if (accepted) {
    res.cookie('cookieConsent', 'true', {
      maxAge: 1000 * 60 * 60 * 24 * 365,  // 1년
      httpOnly: false  // 클라이언트에서 확인 필요
    });
  }

  res.json({ message: '동의 처리 완료' });
});

// 동의 여부에 따른 처리
app.get('/track', (req, res) =&amp;gt; {
  if (!req.cookieConsent) {
    return res.json({ tracked: false, reason: 'no consent' });
  }

  // 추적 로직 실행
  res.json({ tracked: true });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키는 HTTP의 무상태 특성을 보완하여 사용자 상태를 유지하는 핵심 메커니즘입니다. Express.js에서 cookie-parser 미들웨어를 사용하면 쿠키를 쉽게 설정하고 읽을 수 있습니다. 보안을 위해 httpOnly, secure, sameSite 옵션을 적절히 설정하고, 민감한 데이터는 서명된 쿠키를 사용하거나 서버에 저장하는 것이 좋습니다. 실무에서는 환경별 설정 분리, 유틸리티 함수 활용, 쿠키 크기 제한 준수를 통해 안전하고 효율적인 쿠키 관리가 가능합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/820</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BF%A0%ED%82%A4-%EA%B4%80%EB%A6%ACCookie-Management#entry820comment</comments>
      <pubDate>Mon, 16 Mar 2026 08:00:24 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 세션 관리(Session Management)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%ACSession-Management</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 세션이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 서버에서 사용자의 상태를 유지하기 위한 메커니즘입니다. HTTP는 무상태(stateless) 프로토콜이므로, 세션을 통해 로그인 상태, 장바구니 등의 정보를 유지합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 세션 vs 토큰&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;세션 (Stateful):
- 서버에 상태 저장
- 세션 ID만 클라이언트에 저장 (쿠키)
- 서버 메모리/DB 필요
- 확장 시 세션 공유 필요

토큰 (Stateless):
- 클라이언트에 상태 저장
- JWT 등 자체 포함 토큰
- 서버 저장소 불필요
- 토큰 취소 어려움&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. express-session 기본 사용&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install express-session&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 설정&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const session = require('express-session');

const app = express();

app.use(session({
  secret: 'your-secret-key',     // 세션 암호화 키
  resave: false,                 // 변경 없어도 저장할지
  saveUninitialized: false,      // 초기화 안된 세션 저장할지
  cookie: {
    secure: false,               // HTTPS에서만 쿠키 전송
    httpOnly: true,              // JavaScript 접근 금지
    maxAge: 1000 * 60 * 60 * 24  // 1일
  }
}));

// 세션 사용
app.get('/set', (req, res) =&amp;gt; {
  req.session.username = 'hong';
  req.session.visits = (req.session.visits || 0) + 1;
  res.json({ message: 'Session set', visits: req.session.visits });
});

app.get('/get', (req, res) =&amp;gt; {
  res.json({
    username: req.session.username,
    visits: req.session.visits
  });
});

app.get('/destroy', (req, res) =&amp;gt; {
  req.session.destroy((err) =&amp;gt; {
    if (err) {
      return res.status(500).json({ error: 'Failed to destroy session' });
    }
    res.clearCookie('connect.sid');
    res.json({ message: 'Session destroyed' });
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 세션 옵션&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;app.use(session({
  secret: 'my-secret',
  name: 'sessionId',           // 쿠키 이름 (기본: connect.sid)
  resave: false,
  saveUninitialized: false,
  rolling: true,               // 요청마다 만료 시간 갱신
  cookie: {
    path: '/',                 // 쿠키 경로
    httpOnly: true,            // JS 접근 금지
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',        // CSRF 방지
    maxAge: 24 * 60 * 60 * 1000,  // 1일
    domain: 'example.com'      // 쿠키 도메인
  }
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 세션 스토어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 메모리 스토어 (기본)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발용으로만 사용합니다. 프로덕션에서는 사용하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 기본 설정은 메모리 스토어 사용
app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: false
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Redis 스토어&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install connect-redis redis&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const redisClient = createClient({
  url: 'redis://localhost:6379'
});

redisClient.connect().catch(console.error);

app.use(session({
  store: new RedisStore({
    client: redisClient,
    prefix: 'sess:'
  }),
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: false,
    maxAge: 1000 * 60 * 60 * 24  // 1일
  }
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 MongoDB 스토어&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install connect-mongo&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
  store: MongoStore.create({
    mongoUrl: 'mongodb://localhost:27017/sessions',
    ttl: 24 * 60 * 60,  // 1일 (초 단위)
    autoRemove: 'native',
    crypto: {
      secret: 'encryption-secret'
    }
  }),
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 MySQL/PostgreSQL 스토어&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install express-mysql-session
# 또는
npm install connect-pg-simple&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// MySQL
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);

const sessionStore = new MySQLStore({
  host: 'localhost',
  port: 3306,
  user: 'root',
  password: 'password',
  database: 'sessions'
});

app.use(session({
  store: sessionStore,
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
}));&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;4. 인증 시스템 구현&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 1000 * 60 * 60 * 24  // 1일
  }
}));

// 사용자 저장소
const users = new Map();

// 회원가입
app.post('/register', async (req, res) =&amp;gt; {
  const { email, password, name } = req.body;

  if (users.has(email)) {
    return res.status(400).json({ error: 'Email already exists' });
  }

  const hashedPassword = await bcrypt.hash(password, 10);

  const user = {
    id: Date.now().toString(),
    email,
    password: hashedPassword,
    name
  };

  users.set(email, user);

  res.status(201).json({ message: 'User registered' });
});

// 로그인
app.post('/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;

  const user = users.get(email);

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const isValid = await bcrypt.compare(password, user.password);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 세션에 사용자 정보 저장
  req.session.userId = user.id;
  req.session.email = user.email;
  req.session.name = user.name;

  // 세션 재생성 (세션 고정 공격 방지)
  req.session.regenerate((err) =&amp;gt; {
    if (err) {
      return res.status(500).json({ error: 'Session error' });
    }

    req.session.userId = user.id;
    req.session.email = user.email;
    req.session.name = user.name;

    res.json({
      message: 'Login successful',
      user: { id: user.id, email: user.email, name: user.name }
    });
  });
});

// 인증 미들웨어
function isAuthenticated(req, res, next) {
  if (req.session.userId) {
    return next();
  }
  res.status(401).json({ error: 'Not authenticated' });
}

// 프로필
app.get('/profile', isAuthenticated, (req, res) =&amp;gt; {
  res.json({
    user: {
      id: req.session.userId,
      email: req.session.email,
      name: req.session.name
    }
  });
});

// 로그아웃
app.post('/logout', (req, res) =&amp;gt; {
  req.session.destroy((err) =&amp;gt; {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    res.clearCookie('connect.sid');
    res.json({ message: 'Logged out' });
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 세션 보안&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 보안 설정&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;app.use(session({
  secret: process.env.SESSION_SECRET,  // 환경 변수 사용
  name: '__Host-session',              // 보안 쿠키 프리픽스
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,           // HTTPS 필수
    httpOnly: true,         // XSS 방지
    sameSite: 'strict',     // CSRF 방지
    maxAge: 15 * 60 * 1000, // 15분 (짧게 유지)
    path: '/',
    domain: 'example.com'
  }
}));

// 프록시 뒤에서 실행 시
app.set('trust proxy', 1);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 세션 재생성&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 로그인 시 세션 재생성 (세션 고정 공격 방지)
app.post('/login', async (req, res) =&amp;gt; {
  // 인증 로직...

  const oldSession = req.session;

  req.session.regenerate((err) =&amp;gt; {
    if (err) {
      return res.status(500).json({ error: 'Session error' });
    }

    // 필요한 데이터만 새 세션에 복사
    req.session.userId = user.id;

    res.json({ message: 'Login successful' });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 세션 타임아웃&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const SESSION_TIMEOUT = 15 * 60 * 1000;  // 15분

// 활동 추적 미들웨어
app.use((req, res, next) =&amp;gt; {
  if (req.session.userId) {
    const now = Date.now();
    const lastActivity = req.session.lastActivity || now;

    if (now - lastActivity &amp;gt; SESSION_TIMEOUT) {
      // 세션 만료
      return req.session.destroy((err) =&amp;gt; {
        res.status(401).json({ error: 'Session expired' });
      });
    }

    // 활동 시간 갱신
    req.session.lastActivity = now;
  }
  next();
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 플래시 메시지&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install connect-flash&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const flash = require('connect-flash');

app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: false
}));

app.use(flash());

app.post('/login', (req, res) =&amp;gt; {
  // 로그인 실패
  if (!user) {
    req.flash('error', 'Invalid credentials');
    return res.redirect('/login');
  }

  req.flash('success', 'Welcome back!');
  res.redirect('/dashboard');
});

app.get('/login', (req, res) =&amp;gt; {
  res.render('login', {
    error: req.flash('error'),
    success: req.flash('success')
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 다중 서버 환경&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 Sticky Session (로드밸런서)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# Nginx 설정
upstream app {
    ip_hash;  # 같은 IP는 같은 서버로
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

server {
    listen 80;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 공유 세션 스토어 (Redis)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 모든 서버가 같은 Redis를 사용
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const redisClient = createClient({
  url: 'redis://redis-server:6379'
});

redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'shared-secret',
  resave: false,
  saveUninitialized: false
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 세션 관리 API&lt;/h2&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 현재 세션 정보
app.get('/session/info', isAuthenticated, (req, res) =&amp;gt; {
  res.json({
    id: req.sessionID,
    cookie: req.session.cookie,
    data: {
      userId: req.session.userId,
      createdAt: req.session.createdAt
    }
  });
});

// 세션 갱신
app.post('/session/refresh', isAuthenticated, (req, res) =&amp;gt; {
  req.session.touch();  // 만료 시간 갱신
  res.json({ message: 'Session refreshed' });
});

// 모든 세션 종료 (Redis 사용 시)
app.post('/session/logout-all', isAuthenticated, async (req, res) =&amp;gt; {
  const userId = req.session.userId;

  // 현재 세션 삭제
  req.session.destroy();

  // Redis에서 해당 사용자의 모든 세션 삭제
  const keys = await redisClient.keys('sess:*');

  for (const key of keys) {
    const session = await redisClient.get(key);
    const parsed = JSON.parse(session);

    if (parsed.userId === userId) {
      await redisClient.del(key);
    }
  }

  res.json({ message: 'Logged out from all devices' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Passport.js와 함께 사용&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

// Passport 초기화
app.use(passport.initialize());
app.use(passport.session());

// 사용자 직렬화
passport.serializeUser((user, done) =&amp;gt; {
  done(null, user.id);
});

passport.deserializeUser((id, done) =&amp;gt; {
  const user = findUserById(id);
  done(null, user);
});

// 로컬 전략
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  async (email, password, done) =&amp;gt; {
    const user = users.get(email);

    if (!user) {
      return done(null, false, { message: 'User not found' });
    }

    const isValid = await bcrypt.compare(password, user.password);

    if (!isValid) {
      return done(null, false, { message: 'Invalid password' });
    }

    return done(null, user);
  }
));

// 로그인
app.post('/login',
  passport.authenticate('local'),
  (req, res) =&amp;gt; {
    res.json({ message: 'Login successful', user: req.user });
  }
);

// 보호된 라우트
app.get('/profile', (req, res) =&amp;gt; {
  if (!req.isAuthenticated()) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ user: req.user });
});

// 로그아웃
app.post('/logout', (req, res) =&amp;gt; {
  req.logout((err) =&amp;gt; {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    res.json({ message: 'Logged out' });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 세션 vs JWT 선택 기준&lt;/h2&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;세션 사용:
- 전통적인 웹 애플리케이션
- 즉시 로그아웃/권한 취소 필요
- 서버 측 상태 관리 필요
- 소규모 사용자 기반

JWT 사용:
- SPA, 모바일 앱
- 마이크로서비스 아키텍처
- 상태 비저장 서버 필요
- 대규모 분산 시스템&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 서버에서 사용자 상태를 관리하는 전통적인 방법입니다. express-session으로 기본 세션을 구현하고, Redis나 MongoDB 스토어를 사용하여 프로덕션 환경을 지원합니다. 보안을 위해 secure, httpOnly, sameSite 쿠키 옵션을 설정하고, 세션 재생성으로 세션 고정 공격을 방지합니다. 다중 서버 환경에서는 공유 세션 스토어를 사용합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/819</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%ACSession-Management#entry819comment</comments>
      <pubDate>Sun, 15 Mar 2026 20:00:57 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 OAuth2.0 구현</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-OAuth20-%EA%B5%AC%ED%98%84</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. OAuth2.0이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2.0은 제3자 애플리케이션이 사용자의 리소스에 안전하게 접근할 수 있도록 하는 인가 프레임워크입니다. 사용자는 비밀번호를 공유하지 않고 특정 리소스에 대한 접근 권한을 부여할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 OAuth2.0 역할&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Resource Owner: 사용자 (자원 소유자)
Client: 애플리케이션 (접근 요청자)
Authorization Server: 인가 서버 (토큰 발급)
Resource Server: 자원 서버 (보호된 API)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 주요 Grant Types&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. Authorization Code: 웹 서버 애플리케이션
2. Client Credentials: 서버 간 통신
3. Password: 신뢰할 수 있는 자사 앱
4. Refresh Token: 토큰 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Authorization Code Flow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 일반적인 방식으로, 웹 애플리케이션에서 사용됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 흐름&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용자 &amp;rarr; 클라이언트: 로그인 요청
2. 클라이언트 &amp;rarr; 인가 서버: 인가 요청 (redirect)
3. 사용자 &amp;rarr; 인가 서버: 로그인 및 권한 승인
4. 인가 서버 &amp;rarr; 클라이언트: 인가 코드 (redirect)
5. 클라이언트 &amp;rarr; 인가 서버: 코드로 토큰 교환
6. 인가 서버 &amp;rarr; 클라이언트: 액세스 토큰 발급
7. 클라이언트 &amp;rarr; 리소스 서버: API 요청 (토큰 포함)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Google OAuth 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 설정&lt;/h3&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;npm install passport passport-google-oauth20 express-session&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// Google Cloud Console에서 OAuth 2.0 클라이언트 ID 생성
// https://console.cloud.google.com/apis/credentials

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const CALLBACK_URL = 'http://localhost:3000/auth/google/callback';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Passport 설정&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

const app = express();

// 세션 설정
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false
}));

app.use(passport.initialize());
app.use(passport.session());

// 사용자 저장소
const users = new Map();

// Google 전략 설정
passport.use(new GoogleStrategy(
  {
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: CALLBACK_URL
  },
  async (accessToken, refreshToken, profile, done) =&amp;gt; {
    try {
      // 사용자 찾기 또는 생성
      let user = users.get(profile.id);

      if (!user) {
        user = {
          id: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
          picture: profile.photos[0].value,
          provider: 'google'
        };
        users.set(profile.id, user);
      }

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// 세션 직렬화
passport.serializeUser((user, done) =&amp;gt; {
  done(null, user.id);
});

passport.deserializeUser((id, done) =&amp;gt; {
  const user = users.get(id);
  done(null, user);
});

// 라우트
app.get('/auth/google',
  passport.authenticate('google', {
    scope: ['profile', 'email']
  })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) =&amp;gt; {
    res.redirect('/');
  }
);

app.get('/logout', (req, res) =&amp;gt; {
  req.logout(() =&amp;gt; {
    res.redirect('/');
  });
});

app.get('/profile', ensureAuthenticated, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});

function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.status(401).json({ error: 'Not authenticated' });
}

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. GitHub OAuth 구현&lt;/h2&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;npm install passport-github2&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const GitHubStrategy = require('passport-github2').Strategy;

passport.use(new GitHubStrategy(
  {
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/auth/github/callback'
  },
  async (accessToken, refreshToken, profile, done) =&amp;gt; {
    try {
      let user = users.get(`github:${profile.id}`);

      if (!user) {
        user = {
          id: `github:${profile.id}`,
          username: profile.username,
          name: profile.displayName,
          email: profile.emails?.[0]?.value,
          avatar: profile.photos[0].value,
          provider: 'github'
        };
        users.set(user.id, user);
      }

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

app.get('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  (req, res) =&amp;gt; {
    res.redirect('/');
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. JWT와 함께 사용 (SPA용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 대신 JWT를 사용하는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

const app = express();
const JWT_SECRET = process.env.JWT_SECRET;

passport.use(new GoogleStrategy(
  {
    clientID: GOOGLE_CLIENT_ID,
    clientSecret: GOOGLE_CLIENT_SECRET,
    callbackURL: CALLBACK_URL
  },
  (accessToken, refreshToken, profile, done) =&amp;gt; {
    const user = {
      id: profile.id,
      email: profile.emails[0].value,
      name: profile.displayName
    };
    return done(null, user);
  }
));

app.use(passport.initialize());

// 인가 요청
app.get('/auth/google',
  passport.authenticate('google', {
    session: false,
    scope: ['profile', 'email']
  })
);

// 콜백 - JWT 발급
app.get('/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) =&amp;gt; {
    const token = jwt.sign(
      { userId: req.user.id, email: req.user.email },
      JWT_SECRET,
      { expiresIn: '7d' }
    );

    // SPA로 토큰 전달
    res.redirect(`http://localhost:3001/auth/callback?token=${token}`);
  }
);

// 인증 미들웨어
function authenticate(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

app.get('/api/profile', authenticate, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. OAuth2.0 서버 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자체 OAuth2.0 서버를 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install oauth2-server&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const express = require('express');
const OAuth2Server = require('oauth2-server');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 데이터 저장소 (실제로는 DB 사용)
const clients = new Map([
  ['client_id_1', {
    id: 'client_id_1',
    secret: 'client_secret_1',
    redirectUris: ['http://localhost:3001/callback'],
    grants: ['authorization_code', 'refresh_token']
  }]
]);

const users = new Map([
  ['user1', { id: 'user1', username: 'user1', password: 'password1' }]
]);

const authorizationCodes = new Map();
const accessTokens = new Map();
const refreshTokens = new Map();

// OAuth2 서버 모델
const model = {
  // 클라이언트 가져오기
  getClient: async (clientId, clientSecret) =&amp;gt; {
    const client = clients.get(clientId);
    if (!client) return null;
    if (clientSecret &amp;amp;&amp;amp; client.secret !== clientSecret) return null;

    return {
      id: client.id,
      redirectUris: client.redirectUris,
      grants: client.grants
    };
  },

  // 인가 코드 저장
  saveAuthorizationCode: async (code, client, user) =&amp;gt; {
    const authCode = {
      authorizationCode: code.authorizationCode,
      expiresAt: code.expiresAt,
      redirectUri: code.redirectUri,
      scope: code.scope,
      client: { id: client.id },
      user: { id: user.id }
    };
    authorizationCodes.set(code.authorizationCode, authCode);
    return authCode;
  },

  // 인가 코드 가져오기
  getAuthorizationCode: async (authorizationCode) =&amp;gt; {
    return authorizationCodes.get(authorizationCode);
  },

  // 인가 코드 삭제
  revokeAuthorizationCode: async (code) =&amp;gt; {
    return authorizationCodes.delete(code.authorizationCode);
  },

  // 액세스 토큰 저장
  saveToken: async (token, client, user) =&amp;gt; {
    const savedToken = {
      accessToken: token.accessToken,
      accessTokenExpiresAt: token.accessTokenExpiresAt,
      refreshToken: token.refreshToken,
      refreshTokenExpiresAt: token.refreshTokenExpiresAt,
      scope: token.scope,
      client: { id: client.id },
      user: { id: user.id }
    };

    accessTokens.set(token.accessToken, savedToken);
    if (token.refreshToken) {
      refreshTokens.set(token.refreshToken, savedToken);
    }

    return savedToken;
  },

  // 액세스 토큰 가져오기
  getAccessToken: async (accessToken) =&amp;gt; {
    return accessTokens.get(accessToken);
  },

  // 리프레시 토큰 가져오기
  getRefreshToken: async (refreshToken) =&amp;gt; {
    return refreshTokens.get(refreshToken);
  },

  // 리프레시 토큰 삭제
  revokeToken: async (token) =&amp;gt; {
    return refreshTokens.delete(token.refreshToken);
  },

  // 사용자 검증 (password grant용)
  getUser: async (username, password) =&amp;gt; {
    const user = users.get(username);
    if (!user || user.password !== password) return null;
    return { id: user.id };
  }
};

const oauth = new OAuth2Server({ model });

// 인가 엔드포인트
app.get('/authorize', async (req, res) =&amp;gt; {
  // 로그인 페이지 표시 또는 이미 로그인된 경우 권한 승인 페이지 표시
  const { client_id, redirect_uri, state } = req.query;

  res.send(`
    &amp;lt;form action=&quot;/authorize&quot; method=&quot;post&quot;&amp;gt;
      &amp;lt;input type=&quot;hidden&quot; name=&quot;client_id&quot; value=&quot;${client_id}&quot;&amp;gt;
      &amp;lt;input type=&quot;hidden&quot; name=&quot;redirect_uri&quot; value=&quot;${redirect_uri}&quot;&amp;gt;
      &amp;lt;input type=&quot;hidden&quot; name=&quot;state&quot; value=&quot;${state}&quot;&amp;gt;
      &amp;lt;input type=&quot;hidden&quot; name=&quot;response_type&quot; value=&quot;code&quot;&amp;gt;

      &amp;lt;input type=&quot;text&quot; name=&quot;username&quot; placeholder=&quot;Username&quot;&amp;gt;
      &amp;lt;input type=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;Password&quot;&amp;gt;

      &amp;lt;button type=&quot;submit&quot;&amp;gt;Authorize&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  `);
});

app.post('/authorize', async (req, res) =&amp;gt; {
  const { username, password } = req.body;
  const user = users.get(username);

  if (!user || user.password !== password) {
    return res.status(401).send('Invalid credentials');
  }

  const request = new OAuth2Server.Request(req);
  const response = new OAuth2Server.Response(res);

  try {
    const code = await oauth.authorize(request, response, {
      authenticateHandler: {
        handle: () =&amp;gt; ({ id: user.id })
      }
    });

    const redirectUri = new URL(req.body.redirect_uri);
    redirectUri.searchParams.set('code', code.authorizationCode);
    if (req.body.state) {
      redirectUri.searchParams.set('state', req.body.state);
    }

    res.redirect(redirectUri.toString());
  } catch (error) {
    res.status(error.code || 500).json({ error: error.message });
  }
});

// 토큰 엔드포인트
app.post('/token', async (req, res) =&amp;gt; {
  const request = new OAuth2Server.Request(req);
  const response = new OAuth2Server.Response(res);

  try {
    const token = await oauth.token(request, response);
    res.json({
      access_token: token.accessToken,
      token_type: 'Bearer',
      expires_in: 3600,
      refresh_token: token.refreshToken
    });
  } catch (error) {
    res.status(error.code || 500).json({ error: error.message });
  }
});

// 보호된 리소스
app.get('/api/profile', async (req, res) =&amp;gt; {
  const request = new OAuth2Server.Request(req);
  const response = new OAuth2Server.Response(res);

  try {
    const token = await oauth.authenticate(request, response);
    const user = users.get(token.user.id);
    res.json({ user: { id: user.id, username: user.username } });
  } catch (error) {
    res.status(error.code || 401).json({ error: error.message });
  }
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 다중 제공자 지원&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;

// 사용자 찾기 또는 생성 함수
async function findOrCreateUser(provider, profile) {
  const providerId = `${provider}:${profile.id}`;
  let user = users.get(providerId);

  if (!user) {
    user = {
      id: providerId,
      email: profile.emails?.[0]?.value,
      name: profile.displayName,
      picture: profile.photos?.[0]?.value,
      provider
    };
    users.set(providerId, user);
  }

  return user;
}

// Google
passport.use(new GoogleStrategy(
  {
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback'
  },
  async (accessToken, refreshToken, profile, done) =&amp;gt; {
    const user = await findOrCreateUser('google', profile);
    done(null, user);
  }
));

// GitHub
passport.use(new GitHubStrategy(
  {
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: '/auth/github/callback'
  },
  async (accessToken, refreshToken, profile, done) =&amp;gt; {
    const user = await findOrCreateUser('github', profile);
    done(null, user);
  }
));

// Facebook
passport.use(new FacebookStrategy(
  {
    clientID: process.env.FACEBOOK_CLIENT_ID,
    clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
    callbackURL: '/auth/facebook/callback',
    profileFields: ['id', 'emails', 'name', 'picture']
  },
  async (accessToken, refreshToken, profile, done) =&amp;gt; {
    const user = await findOrCreateUser('facebook', profile);
    done(null, user);
  }
));

// 라우트
['google', 'github', 'facebook'].forEach(provider =&amp;gt; {
  app.get(`/auth/${provider}`,
    passport.authenticate(provider, { scope: ['profile', 'email'] })
  );

  app.get(`/auth/${provider}/callback`,
    passport.authenticate(provider, { failureRedirect: '/login' }),
    (req, res) =&amp;gt; res.redirect('/')
  );
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. PKCE (코드 교환용 증명 키)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA나 모바일 앱을 위한 보안 강화 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const crypto = require('crypto');

// PKCE 코드 생성
function generatePKCE() {
  // Code Verifier: 43-128자의 랜덤 문자열
  const codeVerifier = crypto.randomBytes(32)
    .toString('base64url');

  // Code Challenge: Verifier의 SHA256 해시
  const codeChallenge = crypto.createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return { codeVerifier, codeChallenge };
}

// 클라이언트 측
const { codeVerifier, codeChallenge } = generatePKCE();

// 인가 요청 URL
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'http://localhost:3000/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// codeVerifier는 로컬에 저장
sessionStorage.setItem('code_verifier', codeVerifier);

// 토큰 교환 시 codeVerifier 포함
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: 'http://localhost:3000/callback',
    client_id: 'your-client-id',
    code_verifier: sessionStorage.getItem('code_verifier')
  })
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2.0은 제3자 애플리케이션에 안전하게 접근 권한을 부여하는 표준 프로토콜입니다. Passport.js로 Google, GitHub 등의 소셜 로그인을 쉽게 구현할 수 있습니다. Authorization Code 플로우가 가장 안전하며, SPA에서는 JWT와 함께 사용하거나 PKCE를 적용합니다. 자체 OAuth2.0 서버 구현이 필요하면 oauth2-server 패키지를 사용할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/818</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-OAuth20-%EA%B5%AC%ED%98%84#entry818comment</comments>
      <pubDate>Sun, 15 Mar 2026 08:00:24 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 JSON 웹 토큰(JWT) 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-JSON-%EC%9B%B9-%ED%86%A0%ED%81%B0JWT-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JWT란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token)는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. 디지털 서명이 되어 있어 검증과 신뢰가 가능합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 JWT 구조&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;xxxxx.yyyyy.zzzzz
  │      │     │
Header.Payload.Signature

Header: 토큰 타입, 알고리즘
Payload: 클레임(데이터)
Signature: 서명&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 예시&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// Header
{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}

// Payload
{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;admin&quot;: true,
  &quot;iat&quot;: 1516239022
}

// Signature
HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),
  secret
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 설치 및 기본 사용&lt;/h2&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install jsonwebtoken&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 토큰 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const jwt = require('jsonwebtoken');

const SECRET = 'your-256-bit-secret';

// 기본 토큰 생성
const token = jwt.sign(
  { userId: 123, email: 'user@example.com' },
  SECRET
);

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// 옵션과 함께 생성
const tokenWithOptions = jwt.sign(
  { userId: 123, email: 'user@example.com' },
  SECRET,
  {
    expiresIn: '1h',      // 만료 시간
    issuer: 'myapp',      // 발급자
    audience: 'users',    // 대상
    subject: '123'        // 주제 (사용자 ID)
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 토큰 검증&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const jwt = require('jsonwebtoken');

// 동기 검증
try {
  const decoded = jwt.verify(token, SECRET);
  console.log('검증 성공:', decoded);
} catch (error) {
  if (error.name === 'TokenExpiredError') {
    console.log('토큰 만료');
  } else if (error.name === 'JsonWebTokenError') {
    console.log('유효하지 않은 토큰');
  }
}

// 비동기 검증
jwt.verify(token, SECRET, (err, decoded) =&amp;gt; {
  if (err) {
    console.log('검증 실패:', err.message);
    return;
  }
  console.log('검증 성공:', decoded);
});

// 옵션과 함께 검증
const decoded = jwt.verify(token, SECRET, {
  issuer: 'myapp',
  audience: 'users'
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 토큰 디코딩 (검증 없이)&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// 서명 검증 없이 페이로드 확인
const decoded = jwt.decode(token);
console.log(decoded);

// 헤더와 페이로드 모두 확인
const complete = jwt.decode(token, { complete: true });
console.log(complete.header);
console.log(complete.payload);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Express와 함께 사용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 인증 시스템&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());

const SECRET = process.env.JWT_SECRET || 'your-secret-key';
const users = new Map();

// 회원가입
app.post('/register', async (req, res) =&amp;gt; {
  const { email, password, name } = req.body;

  if (users.has(email)) {
    return res.status(400).json({ error: 'Email already exists' });
  }

  const hashedPassword = await bcrypt.hash(password, 10);

  const user = {
    id: Date.now().toString(),
    email,
    password: hashedPassword,
    name
  };

  users.set(email, user);

  res.status(201).json({ message: 'User registered' });
});

// 로그인
app.post('/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;

  const user = users.get(email);

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const isValid = await bcrypt.compare(password, user.password);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // 토큰 생성
  const token = jwt.sign(
    {
      userId: user.id,
      email: user.email,
      name: user.name
    },
    SECRET,
    { expiresIn: '24h' }
  );

  res.json({ token });
});

// 인증 미들웨어
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.slice(7);

  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// 보호된 라우트
app.get('/profile', authenticate, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 리프레시 토큰&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 액세스 토큰 + 리프레시 토큰&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const ACCESS_SECRET = 'access-secret';
const REFRESH_SECRET = 'refresh-secret';
const refreshTokens = new Set(); // 실제로는 DB에 저장

// 토큰 쌍 생성
function generateTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email },
    ACCESS_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  refreshTokens.add(refreshToken);

  return { accessToken, refreshToken };
}

// 로그인
app.post('/login', async (req, res) =&amp;gt; {
  // ... 인증 로직

  const tokens = generateTokens(user);
  res.json(tokens);
});

// 토큰 갱신
app.post('/refresh', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;

  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const user = findUserById(decoded.userId);

    // 새 액세스 토큰 발급
    const accessToken = jwt.sign(
      { userId: user.id, email: user.email },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken });
  } catch (error) {
    refreshTokens.delete(refreshToken);
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// 로그아웃
app.post('/logout', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;
  refreshTokens.delete(refreshToken);
  res.json({ message: 'Logged out' });
});

// 모든 기기에서 로그아웃
app.post('/logout-all', authenticate, (req, res) =&amp;gt; {
  // 해당 사용자의 모든 리프레시 토큰 삭제
  for (const token of refreshTokens) {
    try {
      const decoded = jwt.verify(token, REFRESH_SECRET);
      if (decoded.userId === req.user.userId) {
        refreshTokens.delete(token);
      }
    } catch (e) {
      refreshTokens.delete(token);
    }
  }
  res.json({ message: 'Logged out from all devices' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 토큰 블랙리스트&lt;/h2&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const blacklist = new Set(); // 실제로는 Redis 사용 권장

// 토큰 블랙리스트에 추가
function addToBlacklist(token) {
  const decoded = jwt.decode(token);
  const exp = decoded.exp * 1000; // 만료 시간

  blacklist.add(token);

  // 만료 시간이 지나면 자동 삭제
  setTimeout(() =&amp;gt; {
    blacklist.delete(token);
  }, exp - Date.now());
}

// 블랙리스트 확인 미들웨어
function checkBlacklist(req, res, next) {
  const token = req.headers.authorization?.slice(7);

  if (blacklist.has(token)) {
    return res.status(401).json({ error: 'Token has been revoked' });
  }

  next();
}

// 로그아웃
app.post('/logout', authenticate, (req, res) =&amp;gt; {
  const token = req.headers.authorization.slice(7);
  addToBlacklist(token);
  res.json({ message: 'Logged out' });
});

// 보호된 라우트에 블랙리스트 확인 추가
app.get('/profile', checkBlacklist, authenticate, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. RS256 (비대칭 키)&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const jwt = require('jsonwebtoken');
const fs = require('fs');

// 키 생성: openssl genrsa -out private.pem 2048
// 공개키 추출: openssl rsa -in private.pem -pubout -out public.pem

const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');

// 토큰 생성 (개인키 사용)
const token = jwt.sign(
  { userId: 123 },
  privateKey,
  { algorithm: 'RS256', expiresIn: '1h' }
);

// 토큰 검증 (공개키 사용)
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256']
});

console.log(decoded);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 클레임 (Claims)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 등록된 클레임&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const token = jwt.sign(
  {
    // 등록된 클레임 (예약어)
    iss: 'myapp',           // 발급자 (issuer)
    sub: '1234567890',      // 주제 (subject, 보통 사용자 ID)
    aud: 'users',           // 대상 (audience)
    exp: Math.floor(Date.now() / 1000) + 3600, // 만료 시간
    nbf: Math.floor(Date.now() / 1000),        // 사용 시작 시간
    iat: Math.floor(Date.now() / 1000),        // 발급 시간
    jti: 'unique-token-id'  // JWT ID (토큰 고유 식별자)
  },
  SECRET
);

// 또는 옵션으로 설정
const tokenWithOptions = jwt.sign(
  { data: 'value' },
  SECRET,
  {
    issuer: 'myapp',
    subject: '1234567890',
    audience: 'users',
    expiresIn: '1h',
    notBefore: '0',
    jwtid: 'unique-id'
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 커스텀 클레임&lt;/h3&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const token = jwt.sign(
  {
    // 커스텀 클레임
    userId: 123,
    email: 'user@example.com',
    role: 'admin',
    permissions: ['read', 'write', 'delete']
  },
  SECRET,
  { expiresIn: '1h' }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;function verifyToken(token) {
  try {
    return { success: true, data: jwt.verify(token, SECRET) };
  } catch (error) {
    switch (error.name) {
      case 'TokenExpiredError':
        return {
          success: false,
          error: 'TOKEN_EXPIRED',
          message: '토큰이 만료되었습니다',
          expiredAt: error.expiredAt
        };

      case 'JsonWebTokenError':
        return {
          success: false,
          error: 'INVALID_TOKEN',
          message: '유효하지 않은 토큰입니다'
        };

      case 'NotBeforeError':
        return {
          success: false,
          error: 'TOKEN_NOT_ACTIVE',
          message: '아직 활성화되지 않은 토큰입니다',
          date: error.date
        };

      default:
        return {
          success: false,
          error: 'UNKNOWN_ERROR',
          message: '알 수 없는 오류가 발생했습니다'
        };
    }
  }
}

// 사용
const result = verifyToken(token);

if (!result.success) {
  console.log('토큰 검증 실패:', result.message);
} else {
  console.log('토큰 데이터:', result.data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 보안 고려사항&lt;/h2&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// 1. 환경 변수로 비밀키 관리
const SECRET = process.env.JWT_SECRET;

if (!SECRET) {
  throw new Error('JWT_SECRET environment variable is required');
}

// 2. 짧은 만료 시간 사용
const token = jwt.sign(payload, SECRET, { expiresIn: '15m' });

// 3. 민감한 정보 포함 금지
// 나쁜 예
jwt.sign({ password: 'secret123' }, SECRET);

// 좋은 예
jwt.sign({ userId: 123, email: 'user@example.com' }, SECRET);

// 4. HTTPS 사용 (토큰 전송 시)

// 5. 토큰 저장 위치
// - 브라우저: HttpOnly 쿠키 권장
// - 모바일: 보안 저장소

// 6. 알고리즘 명시
jwt.verify(token, SECRET, { algorithms: ['HS256'] });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 실전 예시: 완전한 인증 시스템&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());

const config = {
  accessSecret: process.env.ACCESS_SECRET || 'access-secret',
  refreshSecret: process.env.REFRESH_SECRET || 'refresh-secret',
  accessExpiresIn: '15m',
  refreshExpiresIn: '7d'
};

const users = new Map();
const refreshTokens = new Map();

// 헬퍼 함수
function generateAccessToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    config.accessSecret,
    { expiresIn: config.accessExpiresIn }
  );
}

function generateRefreshToken(user) {
  const token = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion || 0 },
    config.refreshSecret,
    { expiresIn: config.refreshExpiresIn }
  );

  refreshTokens.set(token, user.id);
  return token;
}

// 미들웨어
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Access token required' });
  }

  try {
    const token = authHeader.slice(7);
    const decoded = jwt.verify(token, config.accessSecret);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Access token expired' });
    }
    return res.status(401).json({ error: 'Invalid access token' });
  }
}

// 라우트
app.post('/auth/register', async (req, res) =&amp;gt; {
  const { email, password, name } = req.body;

  if (users.has(email)) {
    return res.status(400).json({ error: 'Email already registered' });
  }

  const user = {
    id: Date.now().toString(),
    email,
    password: await bcrypt.hash(password, 10),
    name,
    role: 'user',
    tokenVersion: 0
  };

  users.set(email, user);

  res.status(201).json({ message: 'User registered successfully' });
});

app.post('/auth/login', async (req, res) =&amp;gt; {
  const { email, password } = req.body;
  const user = users.get(email);

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  res.json({
    accessToken: generateAccessToken(user),
    refreshToken: generateRefreshToken(user)
  });
});

app.post('/auth/refresh', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;

  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, config.refreshSecret);
    const user = Array.from(users.values()).find(u =&amp;gt; u.id === decoded.userId);

    if (!user || user.tokenVersion !== decoded.tokenVersion) {
      refreshTokens.delete(refreshToken);
      return res.status(401).json({ error: 'Token revoked' });
    }

    res.json({ accessToken: generateAccessToken(user) });
  } catch (error) {
    refreshTokens.delete(refreshToken);
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

app.post('/auth/logout', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;
  refreshTokens.delete(refreshToken);
  res.json({ message: 'Logged out' });
});

app.get('/me', authenticate, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 상태 비저장 인증을 위한 표준 방식입니다. 헤더, 페이로드, 서명으로 구성되며, jsonwebtoken 패키지로 생성과 검증을 수행합니다. 액세스 토큰은 짧게(15분), 리프레시 토큰은 길게(7일) 설정하여 보안과 편의성을 균형있게 유지합니다. 민감한 정보는 페이로드에 포함하지 않고, 비밀키는 환경 변수로 관리해야 합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/817</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-JSON-%EC%9B%B9-%ED%86%A0%ED%81%B0JWT-%EC%82%AC%EC%9A%A9%EB%B2%95#entry817comment</comments>
      <pubDate>Sat, 14 Mar 2026 20:00:35 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Socket.IO 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-SocketIO-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Socket.IO란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket.IO는 실시간 양방향 이벤트 기반 통신을 위한 라이브러리입니다. WebSocket을 기반으로 하지만, 폴백 메커니즘을 제공하여 WebSocket을 지원하지 않는 환경에서도 동작합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Socket.IO vs WebSocket&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Socket.IO 장점:
- 자동 재연결
- 폴백 메커니즘 (long-polling 등)
- 방(Room) 기능 내장
- 네임스페이스
- 이벤트 기반 통신
- 브로드캐스트 기능
- 바이너리 스트리밍&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 설치 및 기본 설정&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;npm install socket.io socket.io-client&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 서버&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST']
  }
});

io.on('connection', (socket) =&amp;gt; {
  console.log('클라이언트 연결:', socket.id);

  // 이벤트 수신
  socket.on('message', (data) =&amp;gt; {
    console.log('메시지:', data);
  });

  // 이벤트 전송
  socket.emit('welcome', { message: '환영합니다!' });

  // 연결 종료
  socket.on('disconnect', (reason) =&amp;gt; {
    console.log('연결 종료:', socket.id, reason);
  });
});

httpServer.listen(3000, () =&amp;gt; {
  console.log('서버 실행 중: http://localhost:3000');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 클라이언트&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// Node.js 클라이언트
const { io } = require('socket.io-client');

const socket = io('http://localhost:3000');

socket.on('connect', () =&amp;gt; {
  console.log('연결됨:', socket.id);
  socket.emit('message', { text: '안녕하세요!' });
});

socket.on('welcome', (data) =&amp;gt; {
  console.log('서버 메시지:', data.message);
});

socket.on('disconnect', () =&amp;gt; {
  console.log('연결 종료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 브라우저 클라이언트 --&amp;gt;
&amp;lt;script src=&quot;/socket.io/socket.io.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
  const socket = io();

  socket.on('connect', () =&amp;gt; {
    console.log('연결됨:', socket.id);
  });

  socket.on('welcome', (data) =&amp;gt; {
    console.log('환영 메시지:', data.message);
  });

  // 메시지 전송
  function sendMessage(text) {
    socket.emit('message', { text });
  }
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이벤트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 이벤트 전송&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 서버
io.on('connection', (socket) =&amp;gt; {
  // 특정 클라이언트에게 전송
  socket.emit('event', { data: 'value' });

  // 모든 클라이언트에게 전송 (자신 포함)
  io.emit('event', { data: 'value' });

  // 자신을 제외한 모든 클라이언트
  socket.broadcast.emit('event', { data: 'value' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 이벤트 수신과 응답&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// 서버 - 콜백으로 응답
io.on('connection', (socket) =&amp;gt; {
  socket.on('getData', (params, callback) =&amp;gt; {
    const data = { result: 'success', items: [1, 2, 3] };
    callback(data);
  });
});

// 클라이언트 - 콜백 받기
socket.emit('getData', { id: 1 }, (response) =&amp;gt; {
  console.log('응답:', response);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 async/await 사용&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 서버
io.on('connection', (socket) =&amp;gt; {
  socket.on('fetchUser', async (userId, callback) =&amp;gt; {
    try {
      const user = await findUser(userId);
      callback({ success: true, user });
    } catch (err) {
      callback({ success: false, error: err.message });
    }
  });
});

// 클라이언트 (Promise 래핑)
function emitAsync(event, data) {
  return new Promise((resolve) =&amp;gt; {
    socket.emit(event, data, resolve);
  });
}

const response = await emitAsync('fetchUser', 123);
console.log(response);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 방(Rooms)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 방 참가/퇴장&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;io.on('connection', (socket) =&amp;gt; {
  // 방 참가
  socket.on('joinRoom', (roomId) =&amp;gt; {
    socket.join(roomId);
    console.log(`${socket.id}가 ${roomId} 방에 참가`);

    // 방의 다른 사람들에게 알림
    socket.to(roomId).emit('userJoined', { id: socket.id });
  });

  // 방 퇴장
  socket.on('leaveRoom', (roomId) =&amp;gt; {
    socket.leave(roomId);
    socket.to(roomId).emit('userLeft', { id: socket.id });
  });

  // 방에 메시지 전송
  socket.on('roomMessage', ({ roomId, message }) =&amp;gt; {
    io.to(roomId).emit('message', {
      from: socket.id,
      message
    });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 방 정보 조회&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;io.on('connection', (socket) =&amp;gt; {
  // 방의 모든 소켓
  socket.on('getRoomMembers', async (roomId, callback) =&amp;gt; {
    const sockets = await io.in(roomId).fetchSockets();
    const members = sockets.map(s =&amp;gt; ({ id: s.id }));
    callback(members);
  });

  // 현재 참여 중인 방 목록
  socket.on('getMyRooms', (callback) =&amp;gt; {
    callback(Array.from(socket.rooms));
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 네임스페이스&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const { Server } = require('socket.io');

const io = new Server(httpServer);

// 기본 네임스페이스 (/)
io.on('connection', (socket) =&amp;gt; {
  console.log('기본 네임스페이스 연결');
});

// 커스텀 네임스페이스
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) =&amp;gt; {
  console.log('채팅 네임스페이스 연결');

  socket.on('message', (msg) =&amp;gt; {
    chatNs.emit('message', msg);
  });
});

const adminNs = io.of('/admin');
adminNs.use((socket, next) =&amp;gt; {
  // 관리자 인증
  if (socket.handshake.auth.token === 'admin-token') {
    next();
  } else {
    next(new Error('Unauthorized'));
  }
});

adminNs.on('connection', (socket) =&amp;gt; {
  console.log('관리자 네임스페이스 연결');
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 클라이언트에서 네임스페이스 연결
const chatSocket = io('/chat');
const adminSocket = io('/admin', {
  auth: { token: 'admin-token' }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 미들웨어&lt;/h2&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;// 연결 미들웨어
io.use((socket, next) =&amp;gt; {
  const token = socket.handshake.auth.token;

  if (!token) {
    return next(new Error('Authentication required'));
  }

  try {
    const user = verifyToken(token);
    socket.user = user;
    next();
  } catch (err) {
    next(new Error('Invalid token'));
  }
});

// 특정 네임스페이스에만 적용
io.of('/admin').use((socket, next) =&amp;gt; {
  if (socket.user?.role !== 'admin') {
    return next(new Error('Admin access required'));
  }
  next();
});

io.on('connection', (socket) =&amp;gt; {
  console.log('인증된 사용자:', socket.user.name);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 채팅 애플리케이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 서버&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

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

// 사용자 관리
const users = new Map();

io.on('connection', (socket) =&amp;gt; {
  console.log('연결:', socket.id);

  // 로그인
  socket.on('login', ({ username }, callback) =&amp;gt; {
    if (Array.from(users.values()).includes(username)) {
      callback({ success: false, error: '이미 사용 중인 이름입니다' });
      return;
    }

    users.set(socket.id, username);
    socket.username = username;

    callback({ success: true });

    // 입장 알림
    socket.broadcast.emit('userJoined', {
      username,
      users: Array.from(users.values())
    });
  });

  // 채팅 메시지
  socket.on('chatMessage', (message) =&amp;gt; {
    io.emit('chatMessage', {
      username: socket.username,
      message,
      timestamp: new Date().toISOString()
    });
  });

  // 타이핑 표시
  socket.on('typing', () =&amp;gt; {
    socket.broadcast.emit('typing', { username: socket.username });
  });

  socket.on('stopTyping', () =&amp;gt; {
    socket.broadcast.emit('stopTyping', { username: socket.username });
  });

  // 연결 종료
  socket.on('disconnect', () =&amp;gt; {
    const username = users.get(socket.id);
    users.delete(socket.id);

    if (username) {
      io.emit('userLeft', {
        username,
        users: Array.from(users.values())
      });
    }
  });
});

httpServer.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 클라이언트&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;채팅&amp;lt;/title&amp;gt;
  &amp;lt;style&amp;gt;
    #messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; }
    .message { padding: 5px; }
    .system { color: gray; font-style: italic; }
  &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;div id=&quot;loginForm&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;username&quot; placeholder=&quot;이름 입력&quot;&amp;gt;
    &amp;lt;button onclick=&quot;login()&quot;&amp;gt;입장&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;div id=&quot;chatRoom&quot; style=&quot;display: none;&quot;&amp;gt;
    &amp;lt;div id=&quot;users&quot;&amp;gt;접속자: &amp;lt;span id=&quot;userList&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;messages&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;typing&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;messageInput&quot; onkeyup=&quot;handleTyping(event)&quot;&amp;gt;
    &amp;lt;button onclick=&quot;sendMessage()&quot;&amp;gt;전송&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;script src=&quot;/socket.io/socket.io.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;script&amp;gt;
    const socket = io();
    let typingTimeout;

    function login() {
      const username = document.getElementById('username').value;

      socket.emit('login', { username }, (response) =&amp;gt; {
        if (response.success) {
          document.getElementById('loginForm').style.display = 'none';
          document.getElementById('chatRoom').style.display = 'block';
        } else {
          alert(response.error);
        }
      });
    }

    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();

      if (message) {
        socket.emit('chatMessage', message);
        input.value = '';
        socket.emit('stopTyping');
      }
    }

    function handleTyping(e) {
      if (e.key === 'Enter') {
        sendMessage();
        return;
      }

      socket.emit('typing');

      clearTimeout(typingTimeout);
      typingTimeout = setTimeout(() =&amp;gt; {
        socket.emit('stopTyping');
      }, 1000);
    }

    socket.on('chatMessage', (data) =&amp;gt; {
      const messages = document.getElementById('messages');
      messages.innerHTML += `
        &amp;lt;div class=&quot;message&quot;&amp;gt;
          &amp;lt;strong&amp;gt;${data.username}:&amp;lt;/strong&amp;gt; ${data.message}
        &amp;lt;/div&amp;gt;
      `;
      messages.scrollTop = messages.scrollHeight;
    });

    socket.on('userJoined', (data) =&amp;gt; {
      const messages = document.getElementById('messages');
      messages.innerHTML += `
        &amp;lt;div class=&quot;message system&quot;&amp;gt;${data.username}님이 입장했습니다.&amp;lt;/div&amp;gt;
      `;
      document.getElementById('userList').textContent = data.users.join(', ');
    });

    socket.on('userLeft', (data) =&amp;gt; {
      const messages = document.getElementById('messages');
      messages.innerHTML += `
        &amp;lt;div class=&quot;message system&quot;&amp;gt;${data.username}님이 퇴장했습니다.&amp;lt;/div&amp;gt;
      `;
      document.getElementById('userList').textContent = data.users.join(', ');
    });

    socket.on('typing', (data) =&amp;gt; {
      document.getElementById('typing').textContent = `${data.username}님이 입력 중...`;
    });

    socket.on('stopTyping', () =&amp;gt; {
      document.getElementById('typing').textContent = '';
    });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 스케일링 (Redis 어댑터)&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;npm install @socket.io/redis-adapter redis&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const { Server } = require('socket.io');

const io = new Server(httpServer);

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() =&amp;gt; {
  io.adapter(createAdapter(pubClient, subClient));
  console.log('Redis 어댑터 연결됨');
});

// 여러 서버 인스턴스에서 동일하게 동작
io.on('connection', (socket) =&amp;gt; {
  socket.on('message', (msg) =&amp;gt; {
    // 모든 서버 인스턴스의 클라이언트에게 전송
    io.emit('message', msg);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;io.on('connection', (socket) =&amp;gt; {
  // 이벤트 에러 처리
  socket.on('riskyOperation', async (data, callback) =&amp;gt; {
    try {
      const result = await performOperation(data);
      callback({ success: true, result });
    } catch (err) {
      callback({ success: false, error: err.message });
    }
  });

  // 연결 에러
  socket.on('error', (err) =&amp;gt; {
    console.error('소켓 에러:', err);
  });
});

// 서버 레벨 에러
io.engine.on('connection_error', (err) =&amp;gt; {
  console.error('연결 에러:', err);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket.IO는 실시간 통신을 위한 강력한 라이브러리입니다. 자동 재연결, 방(Room), 네임스페이스, 이벤트 기반 통신 등 WebSocket보다 더 많은 기능을 제공합니다. 미들웨어로 인증을 구현하고, Redis 어댑터로 여러 서버에 스케일링할 수 있습니다. 채팅, 실시간 알림, 협업 도구 등 다양한 실시간 애플리케이션에 적합합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/815</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-SocketIO-%EC%82%AC%EC%9A%A9%EB%B2%95#entry815comment</comments>
      <pubDate>Sat, 14 Mar 2026 08:00:32 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 인증 및 인가(Authentication and Authorization)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EC%9D%B8%EA%B0%80Authentication-and-Authorization</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인증과 인가의 차이&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 인증(Authentication)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 사용자가 누구인지 확인하는 과정입니다. &quot;너는 누구니?&quot;라는 질문에 답합니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;인증 방법:
- 아이디/비밀번호
- 소셜 로그인 (Google, GitHub 등)
- 생체 인증
- 인증서
- OTP (일회용 비밀번호)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 인가(Authorization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정입니다. &quot;너는 이걸 할 수 있니?&quot;라는 질문에 답합니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;인가 예시:
- 관리자만 사용자 삭제 가능
- 작성자만 글 수정 가능
- 유료 회원만 프리미엄 콘텐츠 접근 가능&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 인증 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 비밀번호 해싱&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install bcrypt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const bcrypt = require('bcrypt');

// 비밀번호 해싱
async function hashPassword(password) {
  const saltRounds = 10;
  return await bcrypt.hash(password, saltRounds);
}

// 비밀번호 검증
async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

// 사용 예시
async function registerUser(email, password) {
  const hashedPassword = await hashPassword(password);

  const user = {
    email,
    password: hashedPassword,
    createdAt: new Date()
  };

  // 데이터베이스에 저장
  await saveUser(user);

  return { id: user.id, email: user.email };
}

async function loginUser(email, password) {
  const user = await findUserByEmail(email);

  if (!user) {
    throw new Error('User not found');
  }

  const isValid = await verifyPassword(password, user.password);

  if (!isValid) {
    throw new Error('Invalid password');
  }

  return user;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Express 인증 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

const SECRET = 'your-secret-key';
const users = new Map();

// 회원가입
app.post('/register', async (req, res) =&amp;gt; {
  try {
    const { email, password, name } = req.body;

    // 이메일 중복 확인
    if (users.has(email)) {
      return res.status(400).json({ error: 'Email already exists' });
    }

    // 비밀번호 해싱
    const hashedPassword = await bcrypt.hash(password, 10);

    // 사용자 저장
    const user = {
      id: Date.now().toString(),
      email,
      password: hashedPassword,
      name,
      role: 'user'
    };

    users.set(email, user);

    res.status(201).json({
      message: 'User registered',
      user: { id: user.id, email, name }
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// 로그인
app.post('/login', async (req, res) =&amp;gt; {
  try {
    const { email, password } = req.body;

    const user = users.get(email);

    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const isValid = await bcrypt.compare(password, user.password);

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // JWT 토큰 생성
    const token = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      SECRET,
      { expiresIn: '1h' }
    );

    res.json({ token, user: { id: user.id, email, name: user.name } });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인증 미들웨어&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 인증 확인 미들웨어
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// 보호된 라우트
app.get('/profile', authenticate, (req, res) =&amp;gt; {
  res.json({ user: req.user });
});

app.get('/users', authenticate, (req, res) =&amp;gt; {
  const allUsers = Array.from(users.values()).map(u =&amp;gt; ({
    id: u.id,
    email: u.email,
    name: u.name
  }));
  res.json(allUsers);
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 역할 기반 인가 (RBAC)&lt;/h2&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 역할 정의
const ROLES = {
  ADMIN: 'admin',
  MODERATOR: 'moderator',
  USER: 'user'
};

// 권한 정의
const PERMISSIONS = {
  [ROLES.ADMIN]: ['read', 'write', 'delete', 'manage_users'],
  [ROLES.MODERATOR]: ['read', 'write', 'delete'],
  [ROLES.USER]: ['read', 'write']
};

// 권한 확인 미들웨어
function authorize(...allowedRoles) {
  return (req, res, next) =&amp;gt; {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

// 특정 권한 확인
function hasPermission(permission) {
  return (req, res, next) =&amp;gt; {
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const userPermissions = PERMISSIONS[req.user.role] || [];

    if (!userPermissions.includes(permission)) {
      return res.status(403).json({ error: 'Permission denied' });
    }

    next();
  };
}

// 사용
app.delete('/users/:id',
  authenticate,
  authorize(ROLES.ADMIN),
  (req, res) =&amp;gt; {
    // 관리자만 사용자 삭제 가능
    res.json({ message: 'User deleted' });
  }
);

app.post('/posts',
  authenticate,
  hasPermission('write'),
  (req, res) =&amp;gt; {
    // write 권한이 있는 사용자만 게시글 작성 가능
    res.json({ message: 'Post created' });
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 리소스 기반 인가&lt;/h2&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// 게시글 소유자 확인
async function isOwner(req, res, next) {
  const postId = req.params.id;
  const post = await findPostById(postId);

  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }

  if (post.authorId !== req.user.userId) {
    return res.status(403).json({ error: 'Not authorized' });
  }

  req.post = post;
  next();
}

// 소유자 또는 관리자 확인
async function isOwnerOrAdmin(req, res, next) {
  const postId = req.params.id;
  const post = await findPostById(postId);

  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }

  const isOwner = post.authorId === req.user.userId;
  const isAdmin = req.user.role === ROLES.ADMIN;

  if (!isOwner &amp;amp;&amp;amp; !isAdmin) {
    return res.status(403).json({ error: 'Not authorized' });
  }

  req.post = post;
  next();
}

// 사용
app.put('/posts/:id',
  authenticate,
  isOwner,
  (req, res) =&amp;gt; {
    // 작성자만 수정 가능
    res.json({ message: 'Post updated' });
  }
);

app.delete('/posts/:id',
  authenticate,
  isOwnerOrAdmin,
  (req, res) =&amp;gt; {
    // 작성자 또는 관리자만 삭제 가능
    res.json({ message: 'Post deleted' });
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정책 기반 인가&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 정책 정의
const policies = {
  'post:read': (user, post) =&amp;gt; true,

  'post:update': (user, post) =&amp;gt; {
    return user.id === post.authorId || user.role === 'admin';
  },

  'post:delete': (user, post) =&amp;gt; {
    return user.id === post.authorId || user.role === 'admin';
  },

  'user:manage': (user) =&amp;gt; {
    return user.role === 'admin';
  }
};

// 정책 확인 함수
function can(user, action, resource = null) {
  const policy = policies[action];

  if (!policy) {
    return false;
  }

  return policy(user, resource);
}

// 미들웨어
function authorize(action, getResource = null) {
  return async (req, res, next) =&amp;gt; {
    let resource = null;

    if (getResource) {
      resource = await getResource(req);
    }

    if (!can(req.user, action, resource)) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    req.resource = resource;
    next();
  };
}

// 사용
app.put('/posts/:id',
  authenticate,
  authorize('post:update', async (req) =&amp;gt; {
    return await findPostById(req.params.id);
  }),
  (req, res) =&amp;gt; {
    res.json({ message: 'Updated' });
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 토큰 갱신&lt;/h2&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;const REFRESH_SECRET = 'refresh-secret';

// 로그인 시 액세스 토큰과 리프레시 토큰 발급
app.post('/login', async (req, res) =&amp;gt; {
  // ... 인증 로직

  const accessToken = jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // 리프레시 토큰 저장 (DB에 저장 권장)
  refreshTokens.set(refreshToken, user.id);

  res.json({ accessToken, refreshToken });
});

// 토큰 갱신
app.post('/refresh', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;

  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const user = findUserById(decoded.userId);

    const newAccessToken = jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// 로그아웃
app.post('/logout', (req, res) =&amp;gt; {
  const { refreshToken } = req.body;
  refreshTokens.delete(refreshToken);
  res.json({ message: 'Logged out' });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 보안 강화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 Rate Limiting&lt;/h3&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;npm install express-rate-limit&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const rateLimit = require('express-rate-limit');

// 로그인 시도 제한
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 5, // 최대 5회
  message: { error: 'Too many login attempts, try again later' }
});

app.post('/login', loginLimiter, async (req, res) =&amp;gt; {
  // 로그인 로직
});

// 전역 제한
const globalLimiter = rateLimit({
  windowMs: 60 * 1000, // 1분
  max: 100 // 최대 100회
});

app.use(globalLimiter);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 보안 헤더&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install helmet&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const helmet = require('helmet');

app.use(helmet());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 입력 검증&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const { body, validationResult } = require('express-validator');

app.post('/register',
  body('email').isEmail().normalizeEmail(),
  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&amp;amp;])/)
    .withMessage('Password must contain letters, numbers, and special characters'),
  body('name').trim().isLength({ min: 2, max: 50 }),
  (req, res) =&amp;gt; {
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    // 회원가입 로직
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Passport.js&lt;/h2&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;npm install passport passport-local passport-jwt&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

// 로컬 전략 (이메일/비밀번호)
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  async (email, password, done) =&amp;gt; {
    try {
      const user = await findUserByEmail(email);

      if (!user) {
        return done(null, false, { message: 'User not found' });
      }

      const isValid = await bcrypt.compare(password, user.password);

      if (!isValid) {
        return done(null, false, { message: 'Invalid password' });
      }

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

// JWT 전략
passport.use(new JwtStrategy(
  {
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: SECRET
  },
  async (payload, done) =&amp;gt; {
    try {
      const user = await findUserById(payload.userId);

      if (!user) {
        return done(null, false);
      }

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

app.use(passport.initialize());

// 로그인
app.post('/login',
  passport.authenticate('local', { session: false }),
  (req, res) =&amp;gt; {
    const token = jwt.sign({ userId: req.user.id }, SECRET);
    res.json({ token });
  }
);

// 보호된 라우트
app.get('/profile',
  passport.authenticate('jwt', { session: false }),
  (req, res) =&amp;gt; {
    res.json({ user: req.user });
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 사용자 신원 확인, 인가는 권한 확인입니다. bcrypt로 비밀번호를 해싱하고, JWT로 상태 비저장 인증을 구현합니다. 역할 기반(RBAC) 또는 정책 기반 인가로 세밀한 접근 제어를 합니다. Rate limiting, helmet, 입력 검증으로 보안을 강화하고, Passport.js로 다양한 인증 전략을 쉽게 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/816</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EC%9D%B8%EA%B0%80Authentication-and-Authorization#entry816comment</comments>
      <pubDate>Fri, 13 Mar 2026 18:00:01 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 웹소켓(WebSocket) 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 웹소켓이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓은 클라이언트와 서버 간의 양방향 실시간 통신을 가능하게 하는 프로토콜입니다. HTTP와 달리 연결이 유지되며, 서버에서 클라이언트로 데이터를 푸시할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 HTTP vs WebSocket&lt;/h3&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;HTTP:
클라이언트 &amp;rarr; 요청 &amp;rarr; 서버
클라이언트 &amp;larr; 응답 &amp;larr; 서버
(연결 종료)

WebSocket:
클라이언트 &amp;larr;&amp;rarr; 양방향 통신 &amp;larr;&amp;rarr; 서버
(연결 유지)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 웹소켓 사용 사례&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 채팅&lt;/li&gt;
&lt;li&gt;실시간 알림&lt;/li&gt;
&lt;li&gt;주식/암호화폐 가격 업데이트&lt;/li&gt;
&lt;li&gt;멀티플레이어 게임&lt;/li&gt;
&lt;li&gt;협업 도구 (실시간 문서 편집)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ws 라이브러리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 가장 많이 사용되는 WebSocket 라이브러리입니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install ws&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 서버&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) =&amp;gt; {
  console.log('새 클라이언트 연결');

  // 메시지 수신
  ws.on('message', (message) =&amp;gt; {
    console.log('받은 메시지:', message.toString());

    // 에코
    ws.send(`서버 응답: ${message}`);
  });

  // 연결 종료
  ws.on('close', () =&amp;gt; {
    console.log('클라이언트 연결 종료');
  });

  // 에러 처리
  ws.on('error', (error) =&amp;gt; {
    console.error('에러:', error);
  });

  // 연결 환영 메시지
  ws.send('WebSocket 서버에 연결되었습니다!');
});

console.log('WebSocket 서버 실행 중: ws://localhost:8080');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 클라이언트&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// Node.js 클라이언트
const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8080');

ws.on('open', () =&amp;gt; {
  console.log('서버에 연결됨');
  ws.send('안녕하세요!');
});

ws.on('message', (data) =&amp;gt; {
  console.log('서버 메시지:', data.toString());
});

ws.on('close', () =&amp;gt; {
  console.log('연결 종료');
});

ws.on('error', (error) =&amp;gt; {
  console.error('에러:', error);
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 브라우저 클라이언트 --&amp;gt;
&amp;lt;script&amp;gt;
const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () =&amp;gt; {
  console.log('연결됨');
  ws.send('안녕하세요!');
};

ws.onmessage = (event) =&amp;gt; {
  console.log('메시지:', event.data);
};

ws.onclose = () =&amp;gt; {
  console.log('연결 종료');
};

ws.onerror = (error) =&amp;gt; {
  console.error('에러:', error);
};
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Express와 통합&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const { createServer } = require('http');
const WebSocket = require('ws');

const app = express();
const server = createServer(app);
const wss = new WebSocket.Server({ server });

// HTTP 라우트
app.get('/', (req, res) =&amp;gt; {
  res.sendFile(__dirname + '/index.html');
});

// WebSocket 연결
wss.on('connection', (ws) =&amp;gt; {
  console.log('WebSocket 연결');

  ws.on('message', (message) =&amp;gt; {
    console.log('메시지:', message.toString());
  });
});

server.listen(3000, () =&amp;gt; {
  console.log('서버 실행 중: http://localhost:3000');
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 브로드캐스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 전체 클라이언트에 전송&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

// 모든 클라이언트에게 브로드캐스트
function broadcast(data) {
  wss.clients.forEach((client) =&amp;gt; {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

wss.on('connection', (ws) =&amp;gt; {
  ws.on('message', (message) =&amp;gt; {
    // 받은 메시지를 모든 클라이언트에게 전송
    broadcast(message.toString());
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 자신을 제외하고 브로드캐스트&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;function broadcastExcludeSender(sender, data) {
  wss.clients.forEach((client) =&amp;gt; {
    if (client !== sender &amp;amp;&amp;amp; client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

wss.on('connection', (ws) =&amp;gt; {
  ws.on('message', (message) =&amp;gt; {
    broadcastExcludeSender(ws, message.toString());
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 채팅 서버 구현&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

// 클라이언트 관리
const clients = new Map();

function broadcast(message, excludeId = null) {
  const data = JSON.stringify(message);

  wss.clients.forEach((client) =&amp;gt; {
    const clientInfo = clients.get(client);
    if (client.readyState === WebSocket.OPEN &amp;amp;&amp;amp; clientInfo?.id !== excludeId) {
      client.send(data);
    }
  });
}

wss.on('connection', (ws) =&amp;gt; {
  const clientId = Date.now().toString();
  let username = `User${clientId.slice(-4)}`;

  clients.set(ws, { id: clientId, username });

  // 입장 알림
  broadcast({
    type: 'system',
    message: `${username}님이 입장했습니다.`
  });

  // 현재 참가자 수 전송
  ws.send(JSON.stringify({
    type: 'info',
    userCount: wss.clients.size
  }));

  ws.on('message', (data) =&amp;gt; {
    try {
      const message = JSON.parse(data);

      switch (message.type) {
        case 'setUsername':
          const oldName = username;
          username = message.username;
          clients.get(ws).username = username;
          broadcast({
            type: 'system',
            message: `${oldName}님이 ${username}(으)로 이름을 변경했습니다.`
          });
          break;

        case 'chat':
          broadcast({
            type: 'chat',
            username,
            message: message.text,
            timestamp: new Date().toISOString()
          });
          break;

        case 'typing':
          broadcast({
            type: 'typing',
            username
          }, clientId);
          break;
      }
    } catch (e) {
      console.error('메시지 파싱 오류:', e);
    }
  });

  ws.on('close', () =&amp;gt; {
    clients.delete(ws);
    broadcast({
      type: 'system',
      message: `${username}님이 퇴장했습니다.`
    });
  });
});

console.log('채팅 서버 실행 중: ws://localhost:8080');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 방(Room) 기능&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

// 방 관리
const rooms = new Map();

function joinRoom(ws, roomId) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId).add(ws);
  ws.roomId = roomId;
}

function leaveRoom(ws) {
  if (ws.roomId &amp;amp;&amp;amp; rooms.has(ws.roomId)) {
    rooms.get(ws.roomId).delete(ws);
    if (rooms.get(ws.roomId).size === 0) {
      rooms.delete(ws.roomId);
    }
  }
}

function broadcastToRoom(roomId, message, excludeWs = null) {
  if (!rooms.has(roomId)) return;

  rooms.get(roomId).forEach((client) =&amp;gt; {
    if (client !== excludeWs &amp;amp;&amp;amp; client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(message));
    }
  });
}

wss.on('connection', (ws) =&amp;gt; {
  ws.on('message', (data) =&amp;gt; {
    const message = JSON.parse(data);

    switch (message.type) {
      case 'join':
        leaveRoom(ws);
        joinRoom(ws, message.roomId);
        ws.send(JSON.stringify({
          type: 'joined',
          roomId: message.roomId
        }));
        broadcastToRoom(message.roomId, {
          type: 'userJoined',
          userId: ws.id
        }, ws);
        break;

      case 'message':
        broadcastToRoom(ws.roomId, {
          type: 'message',
          text: message.text,
          userId: ws.id
        });
        break;

      case 'leave':
        broadcastToRoom(ws.roomId, {
          type: 'userLeft',
          userId: ws.id
        }, ws);
        leaveRoom(ws);
        break;
    }
  });

  ws.on('close', () =&amp;gt; {
    if (ws.roomId) {
      broadcastToRoom(ws.roomId, {
        type: 'userLeft',
        userId: ws.id
      });
      leaveRoom(ws);
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 인증&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const url = require('url');

const SECRET = 'your-secret-key';

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) =&amp;gt; {
    // URL에서 토큰 추출
    const params = new URLSearchParams(url.parse(info.req.url).query);
    const token = params.get('token');

    if (!token) {
      callback(false, 401, 'Unauthorized');
      return;
    }

    try {
      const decoded = jwt.verify(token, SECRET);
      info.req.user = decoded;
      callback(true);
    } catch (e) {
      callback(false, 401, 'Invalid token');
    }
  }
});

wss.on('connection', (ws, req) =&amp;gt; {
  // 인증된 사용자 정보
  const user = req.user;
  console.log(`${user.username} 연결됨`);

  ws.on('message', (message) =&amp;gt; {
    console.log(`${user.username}: ${message}`);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 핑/퐁 (연결 유지)&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

// 연결 상태 확인 간격 (30초)
const HEARTBEAT_INTERVAL = 30000;

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', (ws) =&amp;gt; {
  ws.isAlive = true;
  ws.on('pong', heartbeat);

  ws.on('message', (message) =&amp;gt; {
    console.log('받은 메시지:', message.toString());
  });
});

// 주기적으로 연결 상태 확인
const interval = setInterval(() =&amp;gt; {
  wss.clients.forEach((ws) =&amp;gt; {
    if (ws.isAlive === false) {
      console.log('응답 없는 클라이언트 종료');
      return ws.terminate();
    }

    ws.isAlive = false;
    ws.ping();
  });
}, HEARTBEAT_INTERVAL);

wss.on('close', () =&amp;gt; {
  clearInterval(interval);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 바이너리 데이터 전송&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const WebSocket = require('ws');
const fs = require('fs');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) =&amp;gt; {
  // 바이너리 타입 설정
  ws.binaryType = 'arraybuffer';

  // 파일 전송
  ws.on('message', (data, isBinary) =&amp;gt; {
    if (isBinary) {
      // 바이너리 데이터 처리
      const buffer = Buffer.from(data);
      fs.writeFileSync('received-file.bin', buffer);
      ws.send('파일 수신 완료');
    } else {
      // 텍스트 메시지 처리
      console.log('텍스트:', data.toString());
    }
  });

  // 이미지 전송
  const imageBuffer = fs.readFileSync('image.png');
  ws.send(imageBuffer);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 재연결 로직 (클라이언트)&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.reconnectInterval = options.reconnectInterval || 3000;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
    this.reconnectAttempts = 0;
    this.handlers = { open: [], message: [], close: [], error: [] };

    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = (e) =&amp;gt; {
      console.log('연결됨');
      this.reconnectAttempts = 0;
      this.handlers.open.forEach(h =&amp;gt; h(e));
    };

    this.ws.onmessage = (e) =&amp;gt; {
      this.handlers.message.forEach(h =&amp;gt; h(e));
    };

    this.ws.onclose = (e) =&amp;gt; {
      console.log('연결 종료');
      this.handlers.close.forEach(h =&amp;gt; h(e));
      this.reconnect();
    };

    this.ws.onerror = (e) =&amp;gt; {
      console.error('에러');
      this.handlers.error.forEach(h =&amp;gt; h(e));
    };
  }

  reconnect() {
    if (this.reconnectAttempts &amp;gt;= this.maxReconnectAttempts) {
      console.log('최대 재연결 시도 횟수 초과');
      return;
    }

    this.reconnectAttempts++;
    console.log(`재연결 시도 ${this.reconnectAttempts}...`);

    setTimeout(() =&amp;gt; {
      this.connect();
    }, this.reconnectInterval);
  }

  on(event, handler) {
    if (this.handlers[event]) {
      this.handlers[event].push(handler);
    }
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }

  close() {
    this.ws.close();
  }
}

// 사용
const ws = new ReconnectingWebSocket('ws://localhost:8080');

ws.on('message', (e) =&amp;gt; {
  console.log('메시지:', e.data);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 실시간 양방향 통신이 필요한 애플리케이션에 필수적인 프로토콜입니다. Node.js의 ws 라이브러리로 쉽게 WebSocket 서버를 구축할 수 있습니다. 브로드캐스트, 방 기능, 인증, 핑/퐁 연결 유지 등을 구현하여 안정적인 실시간 서비스를 만들 수 있습니다. 프로덕션에서는 Socket.IO나 기타 고수준 라이브러리를 사용하면 더 많은 기능을 활용할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/814</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket-%EC%82%AC%EC%9A%A9%EB%B2%95#entry814comment</comments>
      <pubDate>Fri, 13 Mar 2026 08:00:44 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 GraphQL API 만들기</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-GraphQL-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. GraphQL이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL은 Facebook에서 개발한 API 쿼리 언어입니다. REST와 달리 클라이언트가 필요한 데이터만 정확히 요청할 수 있으며, 단일 엔드포인트(/graphql)를 통해 모든 데이터에 접근합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 GraphQL vs REST&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;REST:
GET /users/1
GET /users/1/posts
GET /posts/1/comments
&amp;rarr; 여러 번의 요청, 오버페칭/언더페칭 문제

GraphQL:
POST /graphql
query {
  user(id: 1) {
    name
    posts {
      title
      comments {
        content
      }
    }
  }
}
&amp;rarr; 단일 요청으로 필요한 데이터만 조회&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 설정&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;npm install express @apollo/server graphql&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기본 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 스키마 정의&lt;/h3&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;// schema.js
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    createdAt: String!
  }

  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User
    deleteUser(id: ID!): Boolean!

    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post
    deletePost(id: ID!): Boolean!
  }

  input CreateUserInput {
    name: String!
    email: String!
  }

  input UpdateUserInput {
    name: String
    email: String
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }

  input UpdatePostInput {
    title: String
    content: String
  }
`;

module.exports = typeDefs;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 리졸버 정의&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// resolvers.js
const users = new Map();
const posts = new Map();
const comments = new Map();
let nextUserId = 1;
let nextPostId = 1;

const resolvers = {
  Query: {
    users: () =&amp;gt; Array.from(users.values()),
    user: (_, { id }) =&amp;gt; users.get(id),
    posts: () =&amp;gt; Array.from(posts.values()),
    post: (_, { id }) =&amp;gt; posts.get(id)
  },

  Mutation: {
    createUser: (_, { input }) =&amp;gt; {
      const user = {
        id: String(nextUserId++),
        ...input,
        createdAt: new Date().toISOString()
      };
      users.set(user.id, user);
      return user;
    },

    updateUser: (_, { id, input }) =&amp;gt; {
      const user = users.get(id);
      if (!user) return null;

      const updated = { ...user, ...input };
      users.set(id, updated);
      return updated;
    },

    deleteUser: (_, { id }) =&amp;gt; {
      return users.delete(id);
    },

    createPost: (_, { input }) =&amp;gt; {
      const post = {
        id: String(nextPostId++),
        title: input.title,
        content: input.content,
        authorId: input.authorId,
        createdAt: new Date().toISOString()
      };
      posts.set(post.id, post);
      return post;
    },

    updatePost: (_, { id, input }) =&amp;gt; {
      const post = posts.get(id);
      if (!post) return null;

      const updated = { ...post, ...input };
      posts.set(id, updated);
      return updated;
    },

    deletePost: (_, { id }) =&amp;gt; {
      return posts.delete(id);
    }
  },

  // 필드 리졸버
  User: {
    posts: (user) =&amp;gt; {
      return Array.from(posts.values())
        .filter(post =&amp;gt; post.authorId === user.id);
    }
  },

  Post: {
    author: (post) =&amp;gt; users.get(post.authorId),
    comments: (post) =&amp;gt; {
      return Array.from(comments.values())
        .filter(comment =&amp;gt; comment.postId === post.id);
    }
  },

  Comment: {
    author: (comment) =&amp;gt; users.get(comment.authorId),
    post: (comment) =&amp;gt; posts.get(comment.postId)
  }
};

module.exports = resolvers;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 서버 설정&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// index.js
const express = require('express');
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');

async function startServer() {
  const app = express();

  const server = new ApolloServer({
    typeDefs,
    resolvers
  });

  await server.start();

  app.use(express.json());
  app.use('/graphql', expressMiddleware(server, {
    context: async ({ req }) =&amp;gt; ({
      token: req.headers.authorization
    })
  }));

  app.listen(4000, () =&amp;gt; {
    console.log('GraphQL 서버: http://localhost:4000/graphql');
  });
}

startServer();&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 쿼리 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 데이터 조회&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 모든 사용자 조회
query {
  users {
    id
    name
    email
  }
}

# 특정 사용자와 게시글 조회
query {
  user(id: &quot;1&quot;) {
    name
    email
    posts {
      title
      content
    }
  }
}

# 변수 사용
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    posts {
      title
    }
  }
}
# Variables: { &quot;userId&quot;: &quot;1&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 데이터 변경&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 사용자 생성
mutation {
  createUser(input: {
    name: &quot;홍길동&quot;
    email: &quot;hong@example.com&quot;
  }) {
    id
    name
    email
  }
}

# 게시글 생성
mutation {
  createPost(input: {
    title: &quot;첫 번째 글&quot;
    content: &quot;내용입니다&quot;
    authorId: &quot;1&quot;
  }) {
    id
    title
    author {
      name
    }
  }
}

# 변수 사용
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}
# Variables: { &quot;input&quot;: { &quot;name&quot;: &quot;홍길동&quot;, &quot;email&quot;: &quot;hong@example.com&quot; } }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 고급 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 인증/인가&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// context에서 사용자 정보 추출
const server = new ApolloServer({
  typeDefs,
  resolvers
});

app.use('/graphql', expressMiddleware(server, {
  context: async ({ req }) =&amp;gt; {
    const token = req.headers.authorization?.replace('Bearer ', '');
    let user = null;

    if (token) {
      try {
        const decoded = jwt.verify(token, SECRET);
        user = await findUserById(decoded.userId);
      } catch (e) {
        // 토큰 검증 실패
      }
    }

    return { user };
  }
}));

// 리졸버에서 인증 확인
const resolvers = {
  Mutation: {
    createPost: (_, { input }, context) =&amp;gt; {
      if (!context.user) {
        throw new Error('Authentication required');
      }

      // 게시글 생성
    }
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 디렉티브&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 스키마에 디렉티브 정의
const typeDefs = `#graphql
  directive @auth on FIELD_DEFINITION

  type Query {
    publicData: String
    privateData: String @auth
  }
`;

// 디렉티브 구현
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils');

function authDirective(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) =&amp;gt; {
      const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];

      if (authDirective) {
        const { resolve } = fieldConfig;

        fieldConfig.resolve = async (source, args, context, info) =&amp;gt; {
          if (!context.user) {
            throw new Error('Not authenticated');
          }
          return resolve(source, args, context, info);
        };
      }

      return fieldConfig;
    }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 페이지네이션&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;// Cursor 기반 페이지네이션
const typeDefs = `#graphql
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
    startCursor: String
    endCursor: String
  }

  type PostEdge {
    cursor: String!
    node: Post!
  }

  type PostConnection {
    edges: [PostEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
  }

  type Query {
    posts(first: Int, after: String, last: Int, before: String): PostConnection!
  }
`;

const resolvers = {
  Query: {
    posts: (_, { first = 10, after }) =&amp;gt; {
      const allPosts = Array.from(posts.values());
      let startIndex = 0;

      if (after) {
        const afterIndex = allPosts.findIndex(p =&amp;gt; p.id === after);
        startIndex = afterIndex + 1;
      }

      const slicedPosts = allPosts.slice(startIndex, startIndex + first);

      return {
        edges: slicedPosts.map(post =&amp;gt; ({
          cursor: post.id,
          node: post
        })),
        pageInfo: {
          hasNextPage: startIndex + first &amp;lt; allPosts.length,
          hasPreviousPage: startIndex &amp;gt; 0,
          startCursor: slicedPosts[0]?.id,
          endCursor: slicedPosts[slicedPosts.length - 1]?.id
        },
        totalCount: allPosts.length
      };
    }
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 DataLoader (N+1 문제 해결)&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install dataloader&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const DataLoader = require('dataloader');

// DataLoader 생성
function createLoaders() {
  return {
    userLoader: new DataLoader(async (userIds) =&amp;gt; {
      // 배치로 사용자 조회
      const usersData = await User.find({ _id: { $in: userIds } });
      const userMap = new Map(usersData.map(u =&amp;gt; [u.id, u]));
      return userIds.map(id =&amp;gt; userMap.get(id));
    }),

    postsByUserLoader: new DataLoader(async (userIds) =&amp;gt; {
      const allPosts = await Post.find({ authorId: { $in: userIds } });
      const postsMap = new Map();

      for (const post of allPosts) {
        if (!postsMap.has(post.authorId)) {
          postsMap.set(post.authorId, []);
        }
        postsMap.get(post.authorId).push(post);
      }

      return userIds.map(id =&amp;gt; postsMap.get(id) || []);
    })
  };
}

// Context에 loader 추가
app.use('/graphql', expressMiddleware(server, {
  context: async ({ req }) =&amp;gt; ({
    loaders: createLoaders()
  })
}));

// 리졸버에서 사용
const resolvers = {
  Post: {
    author: (post, _, { loaders }) =&amp;gt; {
      return loaders.userLoader.load(post.authorId);
    }
  },

  User: {
    posts: (user, _, { loaders }) =&amp;gt; {
      return loaders.postsByUserLoader.load(user.id);
    }
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.5 서브스크립션 (실시간 업데이트)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { PubSub } = require('graphql-subscriptions');

const pubsub = new PubSub();
const POST_CREATED = 'POST_CREATED';

const typeDefs = `#graphql
  type Subscription {
    postCreated: Post!
  }
`;

const resolvers = {
  Mutation: {
    createPost: async (_, { input }) =&amp;gt; {
      const post = createPostInDB(input);

      // 이벤트 발행
      pubsub.publish(POST_CREATED, { postCreated: post });

      return post;
    }
  },

  Subscription: {
    postCreated: {
      subscribe: () =&amp;gt; pubsub.asyncIterator([POST_CREATED])
    }
  }
};

// WebSocket 서버 설정
const httpServer = createServer(app);

const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql'
});

useServer({ schema }, wsServer);

httpServer.listen(4000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { GraphQLError } = require('graphql');

const resolvers = {
  Query: {
    user: (_, { id }) =&amp;gt; {
      const user = users.get(id);

      if (!user) {
        throw new GraphQLError('User not found', {
          extensions: {
            code: 'USER_NOT_FOUND',
            argumentName: 'id'
          }
        });
      }

      return user;
    }
  },

  Mutation: {
    createUser: (_, { input }) =&amp;gt; {
      // 이메일 중복 검사
      const exists = Array.from(users.values())
        .some(u =&amp;gt; u.email === input.email);

      if (exists) {
        throw new GraphQLError('Email already exists', {
          extensions: {
            code: 'DUPLICATE_EMAIL',
            email: input.email
          }
        });
      }

      // 생성 로직
    }
  }
};

// 에러 포맷팅
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (error) =&amp;gt; {
    // 에러 로깅
    console.error(error);

    // 내부 에러 숨기기
    if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
      return new GraphQLError('Internal server error');
    }

    return error;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 테스트&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// tests/graphql.test.js
const { createTestClient } = require('apollo-server-testing');
const { ApolloServer } = require('@apollo/server');

const server = new ApolloServer({ typeDefs, resolvers });
const { query, mutate } = createTestClient(server);

describe('GraphQL API', () =&amp;gt; {
  test('사용자 생성', async () =&amp;gt; {
    const CREATE_USER = `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          id
          name
          email
        }
      }
    `;

    const result = await mutate({
      mutation: CREATE_USER,
      variables: {
        input: { name: '홍길동', email: 'hong@example.com' }
      }
    });

    expect(result.errors).toBeUndefined();
    expect(result.data.createUser.name).toBe('홍길동');
  });

  test('사용자 조회', async () =&amp;gt; {
    const GET_USERS = `
      query {
        users {
          id
          name
        }
      }
    `;

    const result = await query({ query: GET_USERS });
    expect(result.data.users).toBeDefined();
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL은 클라이언트가 필요한 데이터를 정확히 요청할 수 있는 강력한 쿼리 언어입니다. Apollo Server로 Node.js에서 쉽게 GraphQL API를 구축할 수 있습니다. 스키마에서 타입을 정의하고, 리졸버에서 데이터를 처리합니다. DataLoader로 N+1 문제를 해결하고, 서브스크립션으로 실시간 기능을 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/813</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-GraphQL-API-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry813comment</comments>
      <pubDate>Thu, 12 Mar 2026 20:00:52 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 RESTful API 만들기</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-RESTful-API-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. REST API란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST(Representational State Transfer)는 웹 서비스를 설계하기 위한 아키텍처 스타일입니다. HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 리소스를 조작하며, 상태를 저장하지 않는(Stateless) 특성을 가집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 REST 설계 원칙&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 리소스 기반 URL 설계
   GET    /users        - 사용자 목록 조회
   GET    /users/1      - 특정 사용자 조회
   POST   /users        - 사용자 생성
   PUT    /users/1      - 사용자 전체 수정
   PATCH  /users/1      - 사용자 부분 수정
   DELETE /users/1      - 사용자 삭제

2. HTTP 상태 코드 활용
   200 - OK (성공)
   201 - Created (생성됨)
   204 - No Content (삭제 성공)
   400 - Bad Request (잘못된 요청)
   401 - Unauthorized (인증 필요)
   403 - Forbidden (권한 없음)
   404 - Not Found (리소스 없음)
   500 - Internal Server Error (서버 오류)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로젝트 설정&lt;/h2&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;mkdir rest-api
cd rest-api
npm init -y
npm install express cors helmet morgan
npm install -D nodemon&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// package.json
{
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node src/index.js&quot;,
    &quot;dev&quot;: &quot;nodemon src/index.js&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기본 구조&lt;/h2&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;src/
├── index.js
├── routes/
│   ├── index.js
│   └── users.js
├── controllers/
│   └── userController.js
├── services/
│   └── userService.js
├── models/
│   └── User.js
├── middleware/
│   ├── errorHandler.js
│   └── validator.js
└── utils/
    └── ApiError.js&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. API 서버 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 메인 서버 파일&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// src/index.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// 미들웨어
app.use(helmet());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 라우트
app.use('/api', routes);

// 404 처리
app.use((req, res) =&amp;gt; {
  res.status(404).json({
    success: false,
    error: 'Not Found',
    message: `Cannot ${req.method} ${req.path}`
  });
});

// 에러 핸들러
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&amp;gt; {
  console.log(`API 서버 실행 중: http://localhost:${PORT}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 라우트 정의&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// src/routes/index.js
const express = require('express');
const userRoutes = require('./users');

const router = express.Router();

router.use('/users', userRoutes);

// API 상태 확인
router.get('/health', (req, res) =&amp;gt; {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

module.exports = router;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/routes/users.js
const express = require('express');
const userController = require('../controllers/userController');
const { validateUser, validateId } = require('../middleware/validator');

const router = express.Router();

router.get('/', userController.getAll);
router.get('/:id', validateId, userController.getById);
router.post('/', validateUser, userController.create);
router.put('/:id', validateId, validateUser, userController.update);
router.patch('/:id', validateId, userController.partialUpdate);
router.delete('/:id', validateId, userController.remove);

module.exports = router;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 컨트롤러&lt;/h3&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// src/controllers/userController.js
const userService = require('../services/userService');
const ApiError = require('../utils/ApiError');

exports.getAll = async (req, res, next) =&amp;gt; {
  try {
    const { page = 1, limit = 10, sort = 'createdAt', order = 'desc' } = req.query;

    const result = await userService.findAll({
      page: parseInt(page),
      limit: parseInt(limit),
      sort,
      order
    });

    res.json({
      success: true,
      data: result.users,
      pagination: {
        page: result.page,
        limit: result.limit,
        total: result.total,
        totalPages: result.totalPages
      }
    });
  } catch (error) {
    next(error);
  }
};

exports.getById = async (req, res, next) =&amp;gt; {
  try {
    const user = await userService.findById(req.params.id);

    if (!user) {
      throw new ApiError(404, 'User not found');
    }

    res.json({
      success: true,
      data: user
    });
  } catch (error) {
    next(error);
  }
};

exports.create = async (req, res, next) =&amp;gt; {
  try {
    const user = await userService.create(req.body);

    res.status(201).json({
      success: true,
      data: user
    });
  } catch (error) {
    next(error);
  }
};

exports.update = async (req, res, next) =&amp;gt; {
  try {
    const user = await userService.update(req.params.id, req.body);

    if (!user) {
      throw new ApiError(404, 'User not found');
    }

    res.json({
      success: true,
      data: user
    });
  } catch (error) {
    next(error);
  }
};

exports.partialUpdate = async (req, res, next) =&amp;gt; {
  try {
    const user = await userService.partialUpdate(req.params.id, req.body);

    if (!user) {
      throw new ApiError(404, 'User not found');
    }

    res.json({
      success: true,
      data: user
    });
  } catch (error) {
    next(error);
  }
};

exports.remove = async (req, res, next) =&amp;gt; {
  try {
    const deleted = await userService.remove(req.params.id);

    if (!deleted) {
      throw new ApiError(404, 'User not found');
    }

    res.status(204).end();
  } catch (error) {
    next(error);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 서비스&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// src/services/userService.js
const User = require('../models/User');

class UserService {
  constructor() {
    this.users = new Map();
    this.nextId = 1;
  }

  async findAll({ page, limit, sort, order }) {
    const allUsers = Array.from(this.users.values());

    // 정렬
    allUsers.sort((a, b) =&amp;gt; {
      const aVal = a[sort];
      const bVal = b[sort];
      const comparison = aVal &amp;lt; bVal ? -1 : aVal &amp;gt; bVal ? 1 : 0;
      return order === 'desc' ? -comparison : comparison;
    });

    // 페이지네이션
    const total = allUsers.length;
    const totalPages = Math.ceil(total / limit);
    const start = (page - 1) * limit;
    const users = allUsers.slice(start, start + limit);

    return { users, page, limit, total, totalPages };
  }

  async findById(id) {
    return this.users.get(parseInt(id));
  }

  async create(data) {
    const user = new User({
      id: this.nextId++,
      ...data,
      createdAt: new Date(),
      updatedAt: new Date()
    });

    this.users.set(user.id, user);
    return user;
  }

  async update(id, data) {
    const userId = parseInt(id);
    const user = this.users.get(userId);

    if (!user) return null;

    const updated = new User({
      ...user,
      ...data,
      id: userId,
      updatedAt: new Date()
    });

    this.users.set(userId, updated);
    return updated;
  }

  async partialUpdate(id, data) {
    return this.update(id, data);
  }

  async remove(id) {
    return this.users.delete(parseInt(id));
  }
}

module.exports = new UserService();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.5 모델&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// src/models/User.js
class User {
  constructor({ id, name, email, phone, role = 'user', createdAt, updatedAt }) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.phone = phone;
    this.role = role;
    this.createdAt = createdAt;
    this.updatedAt = updatedAt;
  }

  toJSON() {
    return {
      id: this.id,
      name: this.name,
      email: this.email,
      phone: this.phone,
      role: this.role,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt
    };
  }
}

module.exports = User;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 미들웨어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 에러 핸들러&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/middleware/errorHandler.js
const ApiError = require('../utils/ApiError');

function errorHandler(err, req, res, next) {
  console.error(err);

  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      success: false,
      error: err.message
    });
  }

  // 검증 에러
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      success: false,
      error: 'Validation Error',
      details: err.details
    });
  }

  // 기본 에러
  res.status(500).json({
    success: false,
    error: 'Internal Server Error'
  });
}

module.exports = errorHandler;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 유효성 검증&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/middleware/validator.js
const ApiError = require('../utils/ApiError');

exports.validateUser = (req, res, next) =&amp;gt; {
  const { name, email } = req.body;
  const errors = [];

  if (!name || name.length &amp;lt; 2) {
    errors.push('Name must be at least 2 characters');
  }

  if (!email || !isValidEmail(email)) {
    errors.push('Valid email is required');
  }

  if (errors.length &amp;gt; 0) {
    const error = new Error('Validation Error');
    error.name = 'ValidationError';
    error.details = errors;
    return next(error);
  }

  next();
};

exports.validateId = (req, res, next) =&amp;gt; {
  const { id } = req.params;

  if (!id || isNaN(parseInt(id))) {
    return next(new ApiError(400, 'Invalid ID parameter'));
  }

  next();
};

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 API 에러 클래스&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// src/utils/ApiError.js
class ApiError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
    this.name = 'ApiError';
  }

  static badRequest(message) {
    return new ApiError(400, message);
  }

  static unauthorized(message = 'Unauthorized') {
    return new ApiError(401, message);
  }

  static forbidden(message = 'Forbidden') {
    return new ApiError(403, message);
  }

  static notFound(message = 'Not Found') {
    return new ApiError(404, message);
  }

  static internal(message = 'Internal Server Error') {
    return new ApiError(500, message);
  }
}

module.exports = ApiError;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 고급 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 필터링과 검색&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// GET /api/users?role=admin&amp;amp;search=john
exports.getAll = async (req, res, next) =&amp;gt; {
  try {
    const { page = 1, limit = 10, role, search } = req.query;

    let users = Array.from(userService.users.values());

    // 필터링
    if (role) {
      users = users.filter(u =&amp;gt; u.role === role);
    }

    // 검색
    if (search) {
      const searchLower = search.toLowerCase();
      users = users.filter(u =&amp;gt;
        u.name.toLowerCase().includes(searchLower) ||
        u.email.toLowerCase().includes(searchLower)
      );
    }

    // 페이지네이션
    const total = users.length;
    const start = (page - 1) * limit;
    const paginatedUsers = users.slice(start, start + parseInt(limit));

    res.json({
      success: true,
      data: paginatedUsers,
      pagination: { page: parseInt(page), limit: parseInt(limit), total }
    });
  } catch (error) {
    next(error);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 관계 리소스&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// src/routes/users.js

// 사용자의 게시글 조회
router.get('/:userId/posts', async (req, res, next) =&amp;gt; {
  try {
    const posts = await postService.findByUserId(req.params.userId);
    res.json({ success: true, data: posts });
  } catch (error) {
    next(error);
  }
});

// 사용자에게 게시글 추가
router.post('/:userId/posts', async (req, res, next) =&amp;gt; {
  try {
    const post = await postService.create({
      ...req.body,
      userId: parseInt(req.params.userId)
    });
    res.status(201).json({ success: true, data: post });
  } catch (error) {
    next(error);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 응답 포맷팅&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// src/middleware/responseFormatter.js
function responseFormatter(req, res, next) {
  const originalJson = res.json.bind(res);

  res.json = (data) =&amp;gt; {
    const formatted = {
      success: true,
      timestamp: new Date().toISOString(),
      path: req.path,
      ...data
    };

    return originalJson(formatted);
  };

  next();
}

module.exports = responseFormatter;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. API 문서화&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 간단한 API 문서 엔드포인트
router.get('/docs', (req, res) =&amp;gt; {
  res.json({
    name: 'User API',
    version: '1.0.0',
    endpoints: [
      { method: 'GET', path: '/api/users', description: '사용자 목록 조회' },
      { method: 'GET', path: '/api/users/:id', description: '사용자 조회' },
      { method: 'POST', path: '/api/users', description: '사용자 생성' },
      { method: 'PUT', path: '/api/users/:id', description: '사용자 수정' },
      { method: 'DELETE', path: '/api/users/:id', description: '사용자 삭제' }
    ]
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 테스트&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// tests/users.test.js
const request = require('supertest');
const app = require('../src/app');

describe('Users API', () =&amp;gt; {
  let userId;

  test('POST /api/users - 사용자 생성', async () =&amp;gt; {
    const response = await request(app)
      .post('/api/users')
      .send({ name: '홍길동', email: 'hong@example.com' })
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.data.name).toBe('홍길동');
    userId = response.body.data.id;
  });

  test('GET /api/users/:id - 사용자 조회', async () =&amp;gt; {
    const response = await request(app)
      .get(`/api/users/${userId}`)
      .expect(200);

    expect(response.body.data.id).toBe(userId);
  });

  test('DELETE /api/users/:id - 사용자 삭제', async () =&amp;gt; {
    await request(app)
      .delete(`/api/users/${userId}`)
      .expect(204);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API는 HTTP 메서드와 상태 코드를 활용하여 리소스를 조작하는 표준적인 방식입니다. Express.js로 라우트, 컨트롤러, 서비스 계층을 분리하여 유지보수하기 쉬운 API를 구축할 수 있습니다. 입력 검증, 에러 처리, 페이지네이션, 필터링을 구현하고, 일관된 응답 형식을 사용하면 클라이언트가 사용하기 편리한 API가 됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/812</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-RESTful-API-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry812comment</comments>
      <pubDate>Thu, 12 Mar 2026 08:00:19 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 서버 사이드 렌더링(SSR)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81SSR</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서버 사이드 렌더링이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 렌더링(SSR)은 웹 페이지를 서버에서 HTML로 완전히 렌더링한 후 클라이언트에 전송하는 방식입니다. 초기 로딩 속도가 빠르고 SEO에 유리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 CSR vs SSR&lt;/h3&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;CSR (Client-Side Rendering):
서버 &amp;rarr; 빈 HTML + JS &amp;rarr; 클라이언트에서 렌더링

SSR (Server-Side Rendering):
서버에서 렌더링 &amp;rarr; 완성된 HTML &amp;rarr; 클라이언트 (하이드레이션)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 SSR의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SEO 최적화 (검색 엔진이 콘텐츠 크롤링 가능)&lt;/li&gt;
&lt;li&gt;빠른 First Contentful Paint (FCP)&lt;/li&gt;
&lt;li&gt;소셜 미디어 미리보기 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 부하 증가&lt;/li&gt;
&lt;li&gt;TTFB(Time To First Byte) 증가&lt;/li&gt;
&lt;li&gt;복잡한 상태 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 SSR 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Express + 템플릿 엔진&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// EJS 템플릿 엔진 설정
app.set('view engine', 'ejs');
app.set('views', './views');

// 데이터 가져오기
async function fetchPosts() {
  // 데이터베이스 또는 API에서 데이터 가져오기
  return [
    { id: 1, title: '첫 번째 글', content: '내용...' },
    { id: 2, title: '두 번째 글', content: '내용...' }
  ];
}

// SSR 라우트
app.get('/', async (req, res) =&amp;gt; {
  const posts = await fetchPosts();

  res.render('index', {
    title: '블로그',
    posts
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- views/index.ejs --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/title&amp;gt;
  &amp;lt;meta name=&quot;description&quot; content=&quot;블로그 메인 페이지&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/h1&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;% posts.forEach(post =&amp;gt; { %&amp;gt;
      &amp;lt;li&amp;gt;
        &amp;lt;h2&amp;gt;&amp;lt;%= post.title %&amp;gt;&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;%= post.content %&amp;gt;&amp;lt;/p&amp;gt;
      &amp;lt;/li&amp;gt;
    &amp;lt;% }) %&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. React SSR&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 React SSR&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install react react-dom&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App').default;

const app = express();

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

app.get('*', (req, res) =&amp;gt; {
  const appHtml = ReactDOMServer.renderToString(
    React.createElement(App)
  );

  const html = `
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;title&amp;gt;React SSR&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;div id=&quot;root&quot;&amp;gt;${appHtml}&amp;lt;/div&amp;gt;
      &amp;lt;script src=&quot;/bundle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  `;

  res.send(html);
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.js
import React from 'react';

function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Hello SSR!&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;서버에서 렌더링되었습니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 데이터 페칭과 함께&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// server.js
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');

const app = express();

async function fetchData() {
  // API 호출
  return { users: [{ id: 1, name: '홍길동' }] };
}

app.get('/', async (req, res) =&amp;gt; {
  // 서버에서 데이터 페칭
  const initialData = await fetchData();

  const App = require('./App').default;
  const appHtml = ReactDOMServer.renderToString(
    React.createElement(App, { initialData })
  );

  const html = `
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;title&amp;gt;React SSR&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;div id=&quot;root&quot;&amp;gt;${appHtml}&amp;lt;/div&amp;gt;
      &amp;lt;script&amp;gt;
        window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
      &amp;lt;/script&amp;gt;
      &amp;lt;script src=&quot;/bundle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  `;

  res.send(html);
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// client.js (하이드레이션)
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';

const initialData = window.__INITIAL_DATA__;

hydrateRoot(
  document.getElementById('root'),
  &amp;lt;App initialData={initialData} /&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Next.js&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 React SSR을 쉽게 구현할 수 있는 프레임워크입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 프로젝트 생성&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;npx create-next-app@latest my-app
cd my-app
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 페이지 라우팅&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/index.js
export default function Home({ posts }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;블로그&amp;lt;/h1&amp;gt;
      {posts.map(post =&amp;gt; (
        &amp;lt;article key={post.id}&amp;gt;
          &amp;lt;h2&amp;gt;{post.title}&amp;lt;/h2&amp;gt;
          &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
        &amp;lt;/article&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}

// 서버 사이드 데이터 페칭
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return {
    props: { posts }
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 정적 생성 (SSG)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// pages/posts/[id].js
export default function Post({ post }) {
  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}

// 빌드 시 생성할 경로
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  const paths = posts.map(post =&amp;gt; ({
    params: { id: post.id.toString() }
  }));

  return { paths, fallback: false };
}

// 빌드 시 데이터 페칭
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`);
  const post = await res.json();

  return {
    props: { post },
    revalidate: 60  // ISR: 60초마다 재생성
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 App Router (Next.js 13+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// app/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store'  // SSR
    // cache: 'force-cache'  // SSG
  });
  return res.json();
}

export default async function HomePage() {
  const posts = await getPosts();

  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;블로그&amp;lt;/h1&amp;gt;
      {posts.map(post =&amp;gt; (
        &amp;lt;article key={post.id}&amp;gt;
          &amp;lt;h2&amp;gt;{post.title}&amp;lt;/h2&amp;gt;
        &amp;lt;/article&amp;gt;
      ))}
    &amp;lt;/main&amp;gt;
  );
}

// 메타데이터
export const metadata = {
  title: '블로그',
  description: '최신 글 목록'
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Vue SSR (Nuxt.js)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 Nuxt.js 프로젝트&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;npx nuxi init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 페이지와 데이터 페칭&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- pages/index.vue --&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;블로그&amp;lt;/h1&amp;gt;
    &amp;lt;article v-for=&quot;post in posts&quot; :key=&quot;post.id&quot;&amp;gt;
      &amp;lt;h2&amp;gt;{{ post.title }}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{{ post.content }}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
const { data: posts } = await useFetch('/api/posts')
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 API 라우트&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// server/api/posts.js
export default defineEventHandler(async (event) =&amp;gt; {
  return [
    { id: 1, title: '첫 번째 글', content: '내용...' },
    { id: 2, title: '두 번째 글', content: '내용...' }
  ];
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 스트리밍 SSR&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 18의 스트리밍 SSR로 점진적 렌더링이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// server.js
const express = require('express');
const React = require('react');
const { renderToPipeableStream } = require('react-dom/server');
const App = require('./App').default;

const app = express();

app.get('/', (req, res) =&amp;gt; {
  res.setHeader('Content-Type', 'text/html');

  const { pipe } = renderToPipeableStream(
    &amp;lt;App /&amp;gt;,
    {
      bootstrapScripts: ['/bundle.js'],
      onShellReady() {
        res.statusCode = 200;
        pipe(res);
      },
      onError(error) {
        console.error(error);
        res.statusCode = 500;
        res.send('Error');
      }
    }
  );
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.js with Suspense
import React, { Suspense } from 'react';

const Comments = React.lazy(() =&amp;gt; import('./Comments'));

function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Article&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Main content...&amp;lt;/p&amp;gt;

      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading comments...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;Comments /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 캐싱 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 페이지 캐싱&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const express = require('express');
const NodeCache = require('node-cache');

const app = express();
const cache = new NodeCache({ stdTTL: 60 }); // 60초 캐시

app.get('/', async (req, res) =&amp;gt; {
  const cacheKey = 'homepage';
  let html = cache.get(cacheKey);

  if (!html) {
    const data = await fetchData();
    html = renderPage(data);
    cache.set(cacheKey, html);
  }

  res.send(html);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 CDN 캐싱&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;app.get('/posts/:id', async (req, res) =&amp;gt; {
  const post = await getPost(req.params.id);
  const html = renderPost(post);

  // CDN 캐싱 헤더
  res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate');
  res.send(html);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. SEO 최적화&lt;/h2&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;// components/SEO.js
function SEO({ title, description, url, image }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;
      &amp;lt;meta name=&quot;description&quot; content={description} /&amp;gt;
      &amp;lt;meta property=&quot;og:title&quot; content={title} /&amp;gt;
      &amp;lt;meta property=&quot;og:description&quot; content={description} /&amp;gt;
      &amp;lt;meta property=&quot;og:url&quot; content={url} /&amp;gt;
      &amp;lt;meta property=&quot;og:image&quot; content={image} /&amp;gt;
      &amp;lt;meta name=&quot;twitter:card&quot; content=&quot;summary_large_image&quot; /&amp;gt;
      &amp;lt;link rel=&quot;canonical&quot; href={url} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

// 사용
function PostPage({ post }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;SEO
        title={post.title}
        description={post.excerpt}
        url={`https://example.com/posts/${post.id}`}
        image={post.thumbnail}
      /&amp;gt;
      &amp;lt;article&amp;gt;
        &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 성능 최적화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 컴포넌트 코드 스플리팅&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Next.js dynamic import
import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() =&amp;gt; import('./HeavyComponent'), {
  loading: () =&amp;gt; &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;,
  ssr: false  // 클라이언트에서만 렌더링
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 이미지 최적화&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Next.js Image
import Image from 'next/image';

function PostImage({ src, alt }) {
  return (
    &amp;lt;Image
      src={src}
      alt={alt}
      width={800}
      height={400}
      priority={true}
      placeholder=&quot;blur&quot;
    /&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 렌더링은 SEO와 초기 로딩 성능이 중요한 웹 애플리케이션에 필수적입니다. React의 경우 Next.js, Vue의 경우 Nuxt.js를 사용하면 SSR을 쉽게 구현할 수 있습니다. 스트리밍 SSR로 점진적 렌더링을 하고, 적절한 캐싱 전략으로 서버 부하를 줄이는 것이 중요합니다. SSG(정적 생성)와 ISR(증분 정적 재생성)을 활용하면 더 나은 성능을 얻을 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/811</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81SSR#entry811comment</comments>
      <pubDate>Wed, 11 Mar 2026 18:00:53 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Fastify 프레임워크</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Fastify-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Fastify란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastify는 Node.js를 위한 고성능 웹 프레임워크입니다. Express보다 빠르며, 스키마 기반 검증, 플러그인 아키텍처, 내장 로깅을 제공합니다. JSON 직렬화가 최적화되어 있어 API 서버에 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install fastify&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;const fastify = require('fastify')({ logger: true });

fastify.get('/', async (request, reply) =&amp;gt; {
  return { hello: 'world' };
});

fastify.listen({ port: 3000 }, (err) =&amp;gt; {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 라우팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const fastify = require('fastify')({ logger: true });

// GET
fastify.get('/', async (request, reply) =&amp;gt; {
  return { message: 'Hello' };
});

// POST
fastify.post('/users', async (request, reply) =&amp;gt; {
  const user = request.body;
  reply.code(201);
  return { created: user };
});

// PUT
fastify.put('/users/:id', async (request, reply) =&amp;gt; {
  const { id } = request.params;
  return { updated: id };
});

// DELETE
fastify.delete('/users/:id', async (request, reply) =&amp;gt; {
  reply.code(204);
  return;
});

// 모든 메서드
fastify.all('/all', async (request, reply) =&amp;gt; {
  return { method: request.method };
});

fastify.listen({ port: 3000 });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 경로 파라미터&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 단일 파라미터
fastify.get('/users/:id', async (request, reply) =&amp;gt; {
  const { id } = request.params;
  return { userId: id };
});

// 여러 파라미터
fastify.get('/users/:userId/posts/:postId', async (request, reply) =&amp;gt; {
  const { userId, postId } = request.params;
  return { userId, postId };
});

// 와일드카드
fastify.get('/files/*', async (request, reply) =&amp;gt; {
  return { path: request.params['*'] };
});

// 정규식 파라미터
fastify.get('/items/:id(^\\d+)', async (request, reply) =&amp;gt; {
  return { id: request.params.id };
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 쿼리 파라미터&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;fastify.get('/search', async (request, reply) =&amp;gt; {
  const { q, page = 1, limit = 10 } = request.query;
  return { query: q, page, limit };
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스키마 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastify는 JSON Schema를 사용한 빠른 검증을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 요청 검증&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const userSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 2 },
      email: { type: 'string', format: 'email' },
      age: { type: 'integer', minimum: 0 }
    }
  },
  params: {
    type: 'object',
    properties: {
      id: { type: 'string', pattern: '^\\d+$' }
    }
  },
  querystring: {
    type: 'object',
    properties: {
      page: { type: 'integer', minimum: 1, default: 1 },
      limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }
    }
  }
};

fastify.post('/users', { schema: userSchema }, async (request, reply) =&amp;gt; {
  // request.body는 이미 검증됨
  return { created: request.body };
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 응답 스키마&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 스키마를 정의하면 JSON 직렬화가 최적화됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const getUserSchema = {
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        email: { type: 'string' },
        createdAt: { type: 'string', format: 'date-time' }
      }
    },
    404: {
      type: 'object',
      properties: {
        error: { type: 'string' },
        message: { type: 'string' }
      }
    }
  }
};

fastify.get('/users/:id', { schema: getUserSchema }, async (request, reply) =&amp;gt; {
  const user = await findUser(request.params.id);
  if (!user) {
    reply.code(404);
    return { error: 'Not Found', message: 'User not found' };
  }
  return user;
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 훅 (Hooks)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastify는 요청 라이프사이클의 다양한 지점에 훅을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 요청 전 훅
fastify.addHook('onRequest', async (request, reply) =&amp;gt; {
  request.startTime = Date.now();
});

// 요청 본문 파싱 후
fastify.addHook('preHandler', async (request, reply) =&amp;gt; {
  // 인증 확인
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'Unauthorized' });
  }
});

// 직렬화 전
fastify.addHook('preSerialization', async (request, reply, payload) =&amp;gt; {
  return { ...payload, timestamp: new Date().toISOString() };
});

// 응답 전송 후
fastify.addHook('onResponse', async (request, reply) =&amp;gt; {
  const duration = Date.now() - request.startTime;
  fastify.log.info(`${request.method} ${request.url} - ${duration}ms`);
});

// 에러 발생 시
fastify.addHook('onError', async (request, reply, error) =&amp;gt; {
  fastify.log.error(error);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 플러그인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 플러그인 등록&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 플러그인 정의
async function myPlugin(fastify, options) {
  fastify.decorate('myUtil', () =&amp;gt; 'utility function');

  fastify.addHook('onRequest', async (request) =&amp;gt; {
    request.customData = options.data;
  });
}

// 등록
fastify.register(myPlugin, { data: 'custom' });

// 사용
fastify.get('/', async (request, reply) =&amp;gt; {
  return {
    util: fastify.myUtil(),
    data: request.customData
  };
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 라우트 플러그인&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// plugins/users.js
async function usersRoutes(fastify, options) {
  const users = new Map();
  let nextId = 1;

  fastify.get('/', async (request, reply) =&amp;gt; {
    return Array.from(users.values());
  });

  fastify.get('/:id', async (request, reply) =&amp;gt; {
    const user = users.get(parseInt(request.params.id));
    if (!user) {
      reply.code(404);
      return { error: 'User not found' };
    }
    return user;
  });

  fastify.post('/', async (request, reply) =&amp;gt; {
    const user = { id: nextId++, ...request.body };
    users.set(user.id, user);
    reply.code(201);
    return user;
  });
}

module.exports = usersRoutes;

// app.js
fastify.register(require('./plugins/users'), { prefix: '/api/users' });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 공식 플러그인&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// CORS
const cors = require('@fastify/cors');
fastify.register(cors, {
  origin: ['http://localhost:3000'],
  methods: ['GET', 'POST', 'PUT', 'DELETE']
});

// 정적 파일
const fastifyStatic = require('@fastify/static');
const path = require('path');
fastify.register(fastifyStatic, {
  root: path.join(__dirname, 'public'),
  prefix: '/public/'
});

// 쿠키
const cookie = require('@fastify/cookie');
fastify.register(cookie, {
  secret: 'my-secret'
});

// JWT
const jwt = require('@fastify/jwt');
fastify.register(jwt, {
  secret: 'supersecret'
});

fastify.post('/login', async (request, reply) =&amp;gt; {
  const token = fastify.jwt.sign({ userId: 1 });
  return { token };
});

fastify.decorate('authenticate', async (request, reply) =&amp;gt; {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.send(err);
  }
});

fastify.get('/protected', {
  preHandler: [fastify.authenticate]
}, async (request, reply) =&amp;gt; {
  return request.user;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 전역 에러 핸들러
fastify.setErrorHandler((error, request, reply) =&amp;gt; {
  fastify.log.error(error);

  if (error.validation) {
    reply.status(400).send({
      error: 'Validation Error',
      message: error.message
    });
    return;
  }

  reply.status(error.statusCode || 500).send({
    error: error.name || 'Internal Server Error',
    message: error.message
  });
});

// 404 핸들러
fastify.setNotFoundHandler((request, reply) =&amp;gt; {
  reply.code(404).send({
    error: 'Not Found',
    message: `Route ${request.method} ${request.url} not found`
  });
});

// 커스텀 에러
class NotFoundError extends Error {
  constructor(message) {
    super(message);
    this.statusCode = 404;
    this.name = 'NotFoundError';
  }
}

fastify.get('/users/:id', async (request, reply) =&amp;gt; {
  const user = await findUser(request.params.id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  return user;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 데코레이터&lt;/h2&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 인스턴스 데코레이터
fastify.decorate('config', {
  appName: 'My App',
  version: '1.0.0'
});

// 요청 데코레이터
fastify.decorateRequest('user', null);

fastify.addHook('preHandler', async (request, reply) =&amp;gt; {
  const token = request.headers.authorization;
  if (token) {
    request.user = await verifyToken(token);
  }
});

// 응답 데코레이터
fastify.decorateReply('success', function(data) {
  return this.send({ success: true, data });
});

fastify.get('/users', async (request, reply) =&amp;gt; {
  const users = await getUsers();
  return reply.success(users);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 타입스크립트 지원&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import Fastify, { FastifyRequest, FastifyReply } from 'fastify';

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserBody {
  name: string;
  email: string;
}

interface UserParams {
  id: string;
}

const fastify = Fastify({ logger: true });

fastify.post&amp;lt;{
  Body: CreateUserBody;
}&amp;gt;('/users', async (request, reply) =&amp;gt; {
  const { name, email } = request.body;
  const user: User = { id: 1, name, email };
  reply.code(201);
  return user;
});

fastify.get&amp;lt;{
  Params: UserParams;
}&amp;gt;('/users/:id', async (request, reply) =&amp;gt; {
  const { id } = request.params;
  return { id: parseInt(id) };
});

fastify.listen({ port: 3000 });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실전 API 서버&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fastify = require('fastify')({ logger: true });

// 플러그인
fastify.register(require('@fastify/cors'));

// 데이터 저장소
const users = new Map();
let nextId = 1;

// 스키마
const createUserSchema = {
  body: {
    type: 'object',
    required: ['name', 'email'],
    properties: {
      name: { type: 'string', minLength: 2, maxLength: 50 },
      email: { type: 'string', format: 'email' }
    }
  },
  response: {
    201: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        email: { type: 'string' },
        createdAt: { type: 'string' }
      }
    }
  }
};

const getUsersSchema = {
  querystring: {
    type: 'object',
    properties: {
      page: { type: 'integer', minimum: 1, default: 1 },
      limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }
    }
  },
  response: {
    200: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          id: { type: 'integer' },
          name: { type: 'string' },
          email: { type: 'string' }
        }
      }
    }
  }
};

// 라우트
fastify.get('/api/users', { schema: getUsersSchema }, async (request, reply) =&amp;gt; {
  const { page, limit } = request.query;
  const allUsers = Array.from(users.values());
  const start = (page - 1) * limit;
  return allUsers.slice(start, start + limit);
});

fastify.get('/api/users/:id', async (request, reply) =&amp;gt; {
  const user = users.get(parseInt(request.params.id));
  if (!user) {
    reply.code(404);
    return { error: 'User not found' };
  }
  return user;
});

fastify.post('/api/users', { schema: createUserSchema }, async (request, reply) =&amp;gt; {
  const user = {
    id: nextId++,
    ...request.body,
    createdAt: new Date().toISOString()
  };
  users.set(user.id, user);
  reply.code(201);
  return user;
});

fastify.put('/api/users/:id', async (request, reply) =&amp;gt; {
  const id = parseInt(request.params.id);
  const user = users.get(id);

  if (!user) {
    reply.code(404);
    return { error: 'User not found' };
  }

  const updated = { ...user, ...request.body, id };
  users.set(id, updated);
  return updated;
});

fastify.delete('/api/users/:id', async (request, reply) =&amp;gt; {
  const id = parseInt(request.params.id);

  if (!users.delete(id)) {
    reply.code(404);
    return { error: 'User not found' };
  }

  reply.code(204);
  return;
});

// 시작
const start = async () =&amp;gt; {
  try {
    await fastify.listen({ port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastify는 고성능 Node.js 웹 프레임워크로, Express보다 빠른 처리 속도를 제공합니다. JSON Schema 기반의 요청/응답 검증으로 안전하고 최적화된 API를 구축할 수 있습니다. 플러그인 아키텍처로 기능을 모듈화하고, 훅 시스템으로 요청 라이프사이클을 세밀하게 제어할 수 있습니다. 성능이 중요한 API 서버에 적합한 선택입니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/810</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Fastify-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#entry810comment</comments>
      <pubDate>Wed, 11 Mar 2026 08:00:32 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 NestJS 프레임워크</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-NestJS-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. NestJS란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS는 TypeScript로 작성된 프로그레시브 Node.js 프레임워크입니다. Angular에서 영감을 받은 아키텍처로, 데코레이터, 의존성 주입, 모듈 시스템을 사용합니다. Express 또는 Fastify를 기반으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install -g @nestjs/cli
nest new my-project&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 모듈 (Module)&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [UsersModule, PostsModule],
  controllers: [],
  providers: [],
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 컨트롤러 (Controller)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  HttpStatus,
  HttpCode,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(@Query('page') page: number = 1) {
    return this.usersService.findAll(page);
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 서비스 (Service)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  private users = new Map&amp;lt;number, User&amp;gt;();
  private nextId = 1;

  findAll(page: number) {
    return Array.from(this.users.values());
  }

  findOne(id: number) {
    const user = this.users.get(id);
    if (!user) {
      throw new NotFoundException(`User ${id} not found`);
    }
    return user;
  }

  create(createUserDto: CreateUserDto) {
    const user = {
      id: this.nextId++,
      ...createUserDto,
      createdAt: new Date(),
    };
    this.users.set(user.id, user);
    return user;
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    const user = this.findOne(id);
    const updated = { ...user, ...updateUserDto };
    this.users.set(id, updated);
    return updated;
  }

  remove(id: number) {
    this.findOne(id);
    this.users.delete(id);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 DTO (Data Transfer Object)&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength, IsOptional } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @IsOptional()
  phone?: string;
}

// users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 의존성 주입&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// database.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class DatabaseService {
  private connection: any;

  async connect() {
    // 데이터베이스 연결
  }

  async query(sql: string) {
    // 쿼리 실행
  }
}

// users.service.ts
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';

@Injectable()
export class UsersService {
  constructor(private readonly db: DatabaseService) {}

  async findAll() {
    return this.db.query('SELECT * FROM users');
  }
}

// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [DatabaseModule],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 미들웨어&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();

    res.on('finish', () =&amp;gt; {
      const duration = Date.now() - start;
      console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
    });

    next();
  }
}

// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';

@Module({
  imports: [UsersModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 가드 (Guards)&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// auth.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);

    if (!token) {
      throw new UnauthorizedException();
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      request.user = payload;
    } catch {
      throw new UnauthorizedException();
    }

    return true;
  }

  private extractToken(request: any): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

// 사용
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 파이프 (Pipes)&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 내장 파이프 사용
import { Controller, Get, Param, ParseIntPipe, Query, DefaultValuePipe } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Get()
  findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ) {
    return this.usersService.findAll(page, limit);
  }
}

// 커스텀 파이프
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseDatePipe implements PipeTransform&amp;lt;string, Date&amp;gt; {
  transform(value: string): Date {
    const date = new Date(value);
    if (isNaN(date.getTime())) {
      throw new BadRequestException('Invalid date format');
    }
    return date;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 인터셉터 (Interceptors)&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() =&amp;gt; {
        console.log(`${request.method} ${request.url} - ${Date.now() - now}ms`);
      }),
    );
  }
}

// 응답 변환 인터셉터
@Injectable()
export class TransformInterceptor&amp;lt;T&amp;gt; implements NestInterceptor&amp;lt;T, Response&amp;lt;T&amp;gt;&amp;gt; {
  intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;Response&amp;lt;T&amp;gt;&amp;gt; {
    return next.handle().pipe(
      map((data) =&amp;gt; ({
        data,
        timestamp: new Date().toISOString(),
        statusCode: context.switchToHttp().getResponse().statusCode,
      })),
    );
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 예외 필터&lt;/h2&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? exception.message
        : 'Internal server error';

    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

// 전역 적용
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 데이터베이스 연동 (TypeORM)&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @nestjs/typeorm typeorm pg&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @CreateDateColumn()
  createdAt: Date;
}

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'user',
      password: 'password',
      database: 'mydb',
      entities: [User],
      synchronize: true,  // 개발용만
    }),
    TypeOrmModule.forFeature([User]),
  ],
})
export class AppModule {}

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository&amp;lt;User&amp;gt;,
  ) {}

  findAll(): Promise&amp;lt;User[]&amp;gt; {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise&amp;lt;User&amp;gt; {
    return this.usersRepository.findOneBy({ id });
  }

  create(createUserDto: CreateUserDto): Promise&amp;lt;User&amp;gt; {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }

  async remove(id: number): Promise&amp;lt;void&amp;gt; {
    await this.usersRepository.delete(id);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 설정 관리&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @nestjs/config&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
  ],
})
export class AppModule {}

// database.module.ts
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) =&amp;gt; ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USER'),
        password: configService.get('DB_PASSWORD'),
        database: configService.get('DB_NAME'),
        autoLoadEntities: true,
        synchronize: configService.get('NODE_ENV') === 'development',
      }),
      inject: [ConfigService],
    }),
  ],
})
export class DatabaseModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 테스팅&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

describe('UsersService', () =&amp;gt; {
  let service: UsersService;

  const mockRepository = {
    find: jest.fn(),
    findOneBy: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () =&amp;gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get&amp;lt;UsersService&amp;gt;(UsersService);
  });

  it('should be defined', () =&amp;gt; {
    expect(service).toBeDefined();
  });

  it('should return all users', async () =&amp;gt; {
    const users = [{ id: 1, name: 'Test' }];
    mockRepository.find.mockResolvedValue(users);

    expect(await service.findAll()).toEqual(users);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS는 TypeScript 기반의 엔터프라이즈급 Node.js 프레임워크입니다. 모듈, 컨트롤러, 서비스로 구성된 계층적 아키텍처와 의존성 주입을 통해 테스트 가능하고 확장성 있는 애플리케이션을 구축할 수 있습니다. 가드, 파이프, 인터셉터, 필터 등의 데코레이터 기반 기능으로 관심사를 분리하고, TypeORM, Mongoose 등 다양한 데이터베이스를 쉽게 연동할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/809</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-NestJS-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#entry809comment</comments>
      <pubDate>Tue, 10 Mar 2026 18:00:47 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Hapi.js 프레임워크</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Hapijs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Hapi.js란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hapi.js는 Walmart에서 개발한 엔터프라이즈급 웹 프레임워크입니다. 설정 기반(configuration-centric) 접근 방식을 사용하며, 입력 검증, 인증, 캐싱 등의 기능이 내장되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @hapi/hapi&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Hapi = require('@hapi/hapi');

const init = async () =&amp;gt; {
  const server = Hapi.server({
    port: 3000,
    host: 'localhost'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler: (request, h) =&amp;gt; {
      return 'Hello Hapi!';
    }
  });

  await server.start();
  console.log('서버 실행 중:', server.info.uri);
};

init();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서버 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 서버 옵션&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const Hapi = require('@hapi/hapi');

const server = Hapi.server({
  port: 3000,
  host: 'localhost',
  routes: {
    cors: true,  // CORS 허용
    validate: {
      failAction: async (request, h, err) =&amp;gt; {
        throw err;  // 검증 실패 시 에러 반환
      }
    }
  },
  debug: {
    request: ['error']  // 에러 로깅
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 서버 이벤트&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 요청 로깅
server.events.on('response', (request) =&amp;gt; {
  console.log(
    `${request.method.toUpperCase()} ${request.path} ` +
    `${request.response.statusCode}`
  );
});

// 시작/종료 이벤트
server.events.on('start', () =&amp;gt; {
  console.log('서버 시작됨');
});

server.events.on('stop', () =&amp;gt; {
  console.log('서버 종료됨');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 라우팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// GET 요청
server.route({
  method: 'GET',
  path: '/',
  handler: (request, h) =&amp;gt; {
    return 'Home';
  }
});

// POST 요청
server.route({
  method: 'POST',
  path: '/users',
  handler: (request, h) =&amp;gt; {
    const user = request.payload;  // 요청 본문
    return h.response({ created: user }).code(201);
  }
});

// 여러 메서드
server.route({
  method: ['GET', 'POST'],
  path: '/multi',
  handler: (request, h) =&amp;gt; {
    return `Method: ${request.method}`;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 경로 파라미터&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 필수 파라미터
server.route({
  method: 'GET',
  path: '/users/{id}',
  handler: (request, h) =&amp;gt; {
    const { id } = request.params;
    return { userId: id };
  }
});

// 선택적 파라미터
server.route({
  method: 'GET',
  path: '/files/{path*}',  // 와일드카드
  handler: (request, h) =&amp;gt; {
    return { path: request.params.path };
  }
});

// 여러 파라미터
server.route({
  method: 'GET',
  path: '/users/{userId}/posts/{postId}',
  handler: (request, h) =&amp;gt; {
    const { userId, postId } = request.params;
    return { userId, postId };
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 쿼리 파라미터&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;server.route({
  method: 'GET',
  path: '/search',
  handler: (request, h) =&amp;gt; {
    const { q, page, limit } = request.query;
    return { query: q, page, limit };
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 요청 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hapi는 Joi를 사용한 강력한 입력 검증을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install joi&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Joi = require('joi');

server.route({
  method: 'POST',
  path: '/users',
  options: {
    validate: {
      payload: Joi.object({
        name: Joi.string().min(2).max(50).required(),
        email: Joi.string().email().required(),
        age: Joi.number().integer().min(0).max(150)
      })
    }
  },
  handler: (request, h) =&amp;gt; {
    return h.response(request.payload).code(201);
  }
});

// 파라미터 검증
server.route({
  method: 'GET',
  path: '/users/{id}',
  options: {
    validate: {
      params: Joi.object({
        id: Joi.number().integer().positive().required()
      })
    }
  },
  handler: (request, h) =&amp;gt; {
    return { id: request.params.id };
  }
});

// 쿼리 검증
server.route({
  method: 'GET',
  path: '/search',
  options: {
    validate: {
      query: Joi.object({
        q: Joi.string().required(),
        page: Joi.number().integer().min(1).default(1),
        limit: Joi.number().integer().min(1).max(100).default(10)
      })
    }
  },
  handler: (request, h) =&amp;gt; {
    return request.query;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 응답 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 응답 툴킷 (h)&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;server.route({
  method: 'GET',
  path: '/response-examples',
  handler: (request, h) =&amp;gt; {
    // 기본 응답
    return 'Hello';

    // 상태 코드 설정
    return h.response({ message: 'Created' }).code(201);

    // 헤더 설정
    return h.response({ data: [] })
      .header('X-Custom', 'value')
      .type('application/json');

    // 리다이렉트
    return h.redirect('/other');

    // 파일 응답
    return h.file('path/to/file.pdf');
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 뷰 렌더링&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @hapi/vision ejs&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Vision = require('@hapi/vision');

await server.register(Vision);

server.views({
  engines: { ejs: require('ejs') },
  path: 'views'
});

server.route({
  method: 'GET',
  path: '/',
  handler: (request, h) =&amp;gt; {
    return h.view('index', {
      title: 'Hapi 앱',
      users: [{ name: '홍길동' }]
    });
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 플러그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hapi의 핵심 기능인 플러그인 시스템입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 플러그인 등록&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 내장 플러그인 (정적 파일)
const Inert = require('@hapi/inert');

await server.register(Inert);

server.route({
  method: 'GET',
  path: '/{param*}',
  handler: {
    directory: {
      path: 'public'
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 커스텀 플러그인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// plugins/logger.js
const loggerPlugin = {
  name: 'logger',
  version: '1.0.0',
  register: async (server, options) =&amp;gt; {
    server.ext('onRequest', (request, h) =&amp;gt; {
      console.log(`${request.method.toUpperCase()} ${request.path}`);
      return h.continue;
    });
  }
};

// 사용
await server.register({
  plugin: loggerPlugin,
  options: {}
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 라우트 플러그인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// plugins/users.js
const usersPlugin = {
  name: 'users',
  version: '1.0.0',
  register: async (server, options) =&amp;gt; {
    server.route([
      {
        method: 'GET',
        path: '/api/users',
        handler: (request, h) =&amp;gt; {
          return [{ id: 1, name: '홍길동' }];
        }
      },
      {
        method: 'GET',
        path: '/api/users/{id}',
        handler: (request, h) =&amp;gt; {
          return { id: request.params.id, name: '홍길동' };
        }
      }
    ]);
  }
};

await server.register(usersPlugin);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 인증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 기본 인증&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @hapi/basic&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Basic = require('@hapi/basic');

await server.register(Basic);

const validate = async (request, username, password) =&amp;gt; {
  // 사용자 검증 로직
  const isValid = username === 'admin' &amp;amp;&amp;amp; password === 'secret';
  const credentials = { name: username };

  return { isValid, credentials };
};

server.auth.strategy('simple', 'basic', { validate });
server.auth.default('simple');

server.route({
  method: 'GET',
  path: '/private',
  handler: (request, h) =&amp;gt; {
    return `Hello, ${request.auth.credentials.name}`;
  }
});

// 인증 없이 접근 가능한 라우트
server.route({
  method: 'GET',
  path: '/public',
  options: {
    auth: false
  },
  handler: (request, h) =&amp;gt; {
    return 'Public content';
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 JWT 인증&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;npm install hapi-auth-jwt2 jsonwebtoken&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const jwt = require('jsonwebtoken');
const HapiJwt = require('hapi-auth-jwt2');

const secret = 'your-secret-key';

await server.register(HapiJwt);

server.auth.strategy('jwt', 'jwt', {
  key: secret,
  validate: async (decoded, request, h) =&amp;gt; {
    // 토큰 검증 로직
    return { isValid: true, credentials: decoded };
  },
  verifyOptions: { algorithms: ['HS256'] }
});

server.auth.default('jwt');

// 로그인 (토큰 발급)
server.route({
  method: 'POST',
  path: '/login',
  options: { auth: false },
  handler: (request, h) =&amp;gt; {
    const { username, password } = request.payload;

    // 사용자 검증
    if (username === 'admin' &amp;amp;&amp;amp; password === 'password') {
      const token = jwt.sign({ username }, secret, { expiresIn: '1h' });
      return { token };
    }

    return h.response({ error: 'Invalid credentials' }).code(401);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 캐싱&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;server.route({
  method: 'GET',
  path: '/cached',
  options: {
    cache: {
      expiresIn: 30 * 1000,  // 30초
      privacy: 'private'
    }
  },
  handler: (request, h) =&amp;gt; {
    return { data: 'cached content', time: Date.now() };
  }
});

// 서버 측 캐싱
const cache = server.cache({
  segment: 'users',
  expiresIn: 60 * 1000
});

server.route({
  method: 'GET',
  path: '/users/{id}',
  handler: async (request, h) =&amp;gt; {
    const { id } = request.params;
    let user = await cache.get(id);

    if (!user) {
      user = await fetchUserFromDB(id);
      await cache.set(id, user);
    }

    return user;
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실전 API 서버&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Hapi = require('@hapi/hapi');
const Joi = require('joi');

const init = async () =&amp;gt; {
  const server = Hapi.server({
    port: 3000,
    host: 'localhost',
    routes: {
      cors: {
        origin: ['*']
      }
    }
  });

  // 데이터 저장소
  const users = new Map();
  let nextId = 1;

  // 로깅
  server.events.on('response', (request) =&amp;gt; {
    console.log(
      `${request.method.toUpperCase()} ${request.path} ` +
      `${request.response.statusCode}`
    );
  });

  // 사용자 목록
  server.route({
    method: 'GET',
    path: '/api/users',
    handler: (request, h) =&amp;gt; {
      return Array.from(users.values());
    }
  });

  // 사용자 조회
  server.route({
    method: 'GET',
    path: '/api/users/{id}',
    options: {
      validate: {
        params: Joi.object({
          id: Joi.number().integer().positive().required()
        })
      }
    },
    handler: (request, h) =&amp;gt; {
      const user = users.get(request.params.id);
      if (!user) {
        return h.response({ error: 'User not found' }).code(404);
      }
      return user;
    }
  });

  // 사용자 생성
  server.route({
    method: 'POST',
    path: '/api/users',
    options: {
      validate: {
        payload: Joi.object({
          name: Joi.string().min(2).max(50).required(),
          email: Joi.string().email().required()
        })
      }
    },
    handler: (request, h) =&amp;gt; {
      const user = {
        id: nextId++,
        ...request.payload,
        createdAt: new Date()
      };
      users.set(user.id, user);
      return h.response(user).code(201);
    }
  });

  // 사용자 수정
  server.route({
    method: 'PUT',
    path: '/api/users/{id}',
    options: {
      validate: {
        params: Joi.object({
          id: Joi.number().integer().positive().required()
        }),
        payload: Joi.object({
          name: Joi.string().min(2).max(50),
          email: Joi.string().email()
        })
      }
    },
    handler: (request, h) =&amp;gt; {
      const { id } = request.params;
      const user = users.get(id);

      if (!user) {
        return h.response({ error: 'User not found' }).code(404);
      }

      const updated = { ...user, ...request.payload };
      users.set(id, updated);
      return updated;
    }
  });

  // 사용자 삭제
  server.route({
    method: 'DELETE',
    path: '/api/users/{id}',
    options: {
      validate: {
        params: Joi.object({
          id: Joi.number().integer().positive().required()
        })
      }
    },
    handler: (request, h) =&amp;gt; {
      const { id } = request.params;

      if (!users.delete(id)) {
        return h.response({ error: 'User not found' }).code(404);
      }

      return h.response().code(204);
    }
  });

  await server.start();
  console.log('Hapi API 서버 실행 중:', server.info.uri);
};

process.on('unhandledRejection', (err) =&amp;gt; {
  console.error(err);
  process.exit(1);
});

init();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hapi.js는 설정 기반의 엔터프라이즈급 웹 프레임워크입니다. Joi를 통한 강력한 입력 검증, 플러그인 시스템, 내장 인증과 캐싱 기능이 특징입니다. 미들웨어 대신 라이프사이클 훅(ext)을 사용하고, 응답 툴킷(h)으로 다양한 응답을 처리합니다. 보안과 안정성이 중요한 엔터프라이즈 환경에 적합합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/808</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Hapijs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#entry808comment</comments>
      <pubDate>Tue, 10 Mar 2026 08:00:20 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Express.js 프레임워크</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Expressjs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Express.js란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express.js는 Node.js를 위한 가장 인기 있는 웹 프레임워크입니다. 미니멀하고 유연한 설계로 웹 애플리케이션과 API를 빠르게 구축할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install express&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

app.get('/', (req, res) =&amp;gt; {
  res.send('Hello World!');
});

app.listen(3000, () =&amp;gt; {
  console.log('서버 실행 중: http://localhost:3000');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 라우팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// HTTP 메서드별 라우트
app.get('/', (req, res) =&amp;gt; {
  res.send('GET 요청');
});

app.post('/users', (req, res) =&amp;gt; {
  res.send('POST 요청');
});

app.put('/users/:id', (req, res) =&amp;gt; {
  res.send('PUT 요청');
});

app.delete('/users/:id', (req, res) =&amp;gt; {
  res.send('DELETE 요청');
});

// 모든 HTTP 메서드
app.all('/api/*', (req, res, next) =&amp;gt; {
  console.log('API 요청:', req.method, req.path);
  next();
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 라우트 파라미터&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 경로 파라미터
app.get('/users/:userId', (req, res) =&amp;gt; {
  const userId = req.params.userId;
  res.send(`사용자 ID: ${userId}`);
});

// 여러 파라미터
app.get('/users/:userId/posts/:postId', (req, res) =&amp;gt; {
  const { userId, postId } = req.params;
  res.send(`사용자 ${userId}의 포스트 ${postId}`);
});

// 선택적 파라미터
app.get('/posts/:year/:month?', (req, res) =&amp;gt; {
  const { year, month } = req.params;
  res.send(`${year}년 ${month || '전체'}월`);
});

// 정규표현식 파라미터
app.get('/users/:id(\\d+)', (req, res) =&amp;gt; {
  // id가 숫자일 때만 매칭
  res.send(`사용자 ID: ${req.params.id}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 라우터 모듈화&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) =&amp;gt; {
  res.json([{ id: 1, name: '홍길동' }]);
});

router.get('/:id', (req, res) =&amp;gt; {
  res.json({ id: req.params.id, name: '홍길동' });
});

router.post('/', (req, res) =&amp;gt; {
  res.status(201).json({ id: 2, ...req.body });
});

module.exports = router;

// app.js
const express = require('express');
const userRoutes = require('./routes/users');

const app = express();
app.use('/users', userRoutes);

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 미들웨어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 미들웨어 기본&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// 애플리케이션 레벨 미들웨어
app.use((req, res, next) =&amp;gt; {
  console.log('요청 시간:', Date.now());
  next();  // 다음 미들웨어로
});

// 특정 경로에만 적용
app.use('/api', (req, res, next) =&amp;gt; {
  console.log('API 요청');
  next();
});

// 여러 미들웨어 체인
app.use('/admin', authenticate, authorize, (req, res) =&amp;gt; {
  res.send('관리자 페이지');
});

function authenticate(req, res, next) {
  // 인증 로직
  next();
}

function authorize(req, res, next) {
  // 권한 검사
  next();
}

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 내장 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// JSON 파싱
app.use(express.json());

// URL 인코딩된 본문 파싱
app.use(express.urlencoded({ extended: true }));

// 정적 파일 서빙
app.use(express.static('public'));
app.use('/static', express.static('files'));

// raw 본문 파싱
app.use(express.raw({ type: 'application/octet-stream' }));

// 텍스트 본문 파싱
app.use(express.text({ type: 'text/plain' }));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 서드파티 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const express = require('express');
const morgan = require('morgan');       // 로깅
const cors = require('cors');           // CORS
const helmet = require('helmet');       // 보안 헤더
const compression = require('compression'); // 압축

const app = express();

app.use(helmet());                      // 보안 헤더 설정
app.use(cors());                        // CORS 허용
app.use(compression());                 // 응답 압축
app.use(morgan('combined'));            // 요청 로깅

// CORS 상세 설정
app.use(cors({
  origin: ['http://localhost:3000', 'https://example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true
}));

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 에러 처리 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

app.get('/error', (req, res, next) =&amp;gt; {
  next(new Error('의도적 에러'));
});

// 비동기 에러 처리
app.get('/async-error', async (req, res, next) =&amp;gt; {
  try {
    await someAsyncOperation();
    res.send('성공');
  } catch (err) {
    next(err);
  }
});

// Express 5에서는 자동 처리
app.get('/async-error2', async (req, res) =&amp;gt; {
  await someAsyncOperation();  // 에러 시 자동으로 에러 핸들러로
  res.send('성공');
});

// 404 핸들러
app.use((req, res, next) =&amp;gt; {
  res.status(404).json({ error: 'Not Found' });
});

// 에러 핸들러 (4개의 인자)
app.use((err, req, res, next) =&amp;gt; {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 요청과 응답&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 요청 객체 (req)&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;app.post('/users', (req, res) =&amp;gt; {
  // 요청 정보
  console.log(req.method);        // POST
  console.log(req.path);          // /users
  console.log(req.originalUrl);   // /users?page=1
  console.log(req.protocol);      // http
  console.log(req.hostname);      // localhost
  console.log(req.ip);            // 127.0.0.1

  // 파라미터
  console.log(req.params);        // { id: '123' }
  console.log(req.query);         // { page: '1' }
  console.log(req.body);          // { name: '홍길동' }

  // 헤더
  console.log(req.headers);       // 전체 헤더
  console.log(req.get('Content-Type'));  // 특정 헤더

  // 쿠키 (cookie-parser 필요)
  console.log(req.cookies);

  res.send('OK');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 응답 객체 (res)&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;app.get('/response-examples', (req, res) =&amp;gt; {
  // 상태 코드
  res.status(200);
  res.status(404);
  res.status(500);

  // 헤더 설정
  res.set('X-Custom-Header', 'value');
  res.set({
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache'
  });

  // 쿠키 설정
  res.cookie('name', 'value', {
    maxAge: 900000,
    httpOnly: true,
    secure: true
  });

  // 다양한 응답
  res.send('텍스트 응답');
  res.json({ message: 'JSON 응답' });
  res.sendFile('/path/to/file.pdf');
  res.download('/path/to/file.pdf', 'download-name.pdf');
  res.redirect('/other-page');
  res.redirect(301, '/permanent-redirect');

  // 체이닝
  res.status(201).json({ created: true });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 다양한 응답 타입&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// JSON 응답
app.get('/api/users', (req, res) =&amp;gt; {
  res.json({ users: [] });
});

// HTML 응답
app.get('/page', (req, res) =&amp;gt; {
  res.send('&amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;');
});

// 파일 다운로드
app.get('/download', (req, res) =&amp;gt; {
  res.download('./files/report.pdf');
});

// 스트리밍 응답
app.get('/stream', (req, res) =&amp;gt; {
  res.setHeader('Content-Type', 'text/plain');
  res.write('첫 번째 청크\n');
  setTimeout(() =&amp;gt; res.write('두 번째 청크\n'), 1000);
  setTimeout(() =&amp;gt; res.end('완료'), 2000);
});

// 리다이렉트
app.get('/old-page', (req, res) =&amp;gt; {
  res.redirect(301, '/new-page');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 템플릿 엔진&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 EJS 설정&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// 뷰 엔진 설정
app.set('view engine', 'ejs');
app.set('views', './views');

app.get('/', (req, res) =&amp;gt; {
  res.render('index', {
    title: '홈페이지',
    users: [
      { name: '홍길동', age: 30 },
      { name: '김철수', age: 25 }
    ]
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- views/index.ejs --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/h1&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;% users.forEach(user =&amp;gt; { %&amp;gt;
      &amp;lt;li&amp;gt;&amp;lt;%= user.name %&amp;gt; (&amp;lt;%= user.age %&amp;gt;세)&amp;lt;/li&amp;gt;
    &amp;lt;% }) %&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실전 API 서버&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const express = require('express');
const app = express();

// 미들웨어
app.use(express.json());

// 데이터 저장소
const users = new Map();
let nextId = 1;

// CRUD API
app.get('/api/users', (req, res) =&amp;gt; {
  const { page = 1, limit = 10 } = req.query;
  const allUsers = Array.from(users.values());
  const start = (page - 1) * limit;
  const paginatedUsers = allUsers.slice(start, start + Number(limit));

  res.json({
    data: paginatedUsers,
    total: allUsers.length,
    page: Number(page),
    limit: Number(limit)
  });
});

app.get('/api/users/:id', (req, res) =&amp;gt; {
  const user = users.get(Number(req.params.id));

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});

app.post('/api/users', (req, res) =&amp;gt; {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }

  const user = { id: nextId++, name, email, createdAt: new Date() };
  users.set(user.id, user);

  res.status(201).json(user);
});

app.put('/api/users/:id', (req, res) =&amp;gt; {
  const id = Number(req.params.id);
  const user = users.get(id);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  const updated = { ...user, ...req.body, id };
  users.set(id, updated);

  res.json(updated);
});

app.delete('/api/users/:id', (req, res) =&amp;gt; {
  const id = Number(req.params.id);

  if (!users.has(id)) {
    return res.status(404).json({ error: 'User not found' });
  }

  users.delete(id);
  res.status(204).end();
});

// 에러 핸들러
app.use((err, req, res, next) =&amp;gt; {
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000, () =&amp;gt; {
  console.log('API 서버 실행 중');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 프로젝트 구조&lt;/h2&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;project/
├── src/
│   ├── controllers/
│   │   └── userController.js
│   ├── routes/
│   │   ├── index.js
│   │   └── userRoutes.js
│   ├── middleware/
│   │   ├── auth.js
│   │   └── errorHandler.js
│   ├── models/
│   │   └── User.js
│   ├── services/
│   │   └── userService.js
│   └── app.js
├── config/
│   └── index.js
├── public/
├── views/
└── package.json&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// src/app.js
const express = require('express');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

app.use(express.json());
app.use('/api', routes);
app.use(errorHandler);

module.exports = app;

// src/routes/index.js
const express = require('express');
const userRoutes = require('./userRoutes');

const router = express.Router();

router.use('/users', userRoutes);

module.exports = router;

// src/controllers/userController.js
const userService = require('../services/userService');

exports.getAll = async (req, res, next) =&amp;gt; {
  try {
    const users = await userService.findAll();
    res.json(users);
  } catch (err) {
    next(err);
  }
};

exports.getById = async (req, res, next) =&amp;gt; {
  try {
    const user = await userService.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'Not found' });
    }
    res.json(user);
  } catch (err) {
    next(err);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 유용한 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 비동기 핸들러 래퍼&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 비동기 에러 자동 처리
const asyncHandler = (fn) =&amp;gt; (req, res, next) =&amp;gt; {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/users', asyncHandler(async (req, res) =&amp;gt; {
  const users = await User.findAll();
  res.json(users);
}));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 요청 검증&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const { body, validationResult } = require('express-validator');

app.post('/users',
  body('email').isEmail(),
  body('password').isLength({ min: 6 }),
  (req, res) =&amp;gt; {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // 처리
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express.js는 Node.js 웹 개발의 사실상 표준 프레임워크입니다. 미들웨어 기반 아키텍처로 유연하게 기능을 추가할 수 있고, 라우팅, 요청/응답 처리, 에러 핸들링 등 웹 서버에 필요한 기능을 간결하게 구현할 수 있습니다. 프로덕션 환경에서는 helmet, cors, compression 등의 미들웨어와 함께 사용하고, 적절한 에러 처리와 로깅을 구현해야 합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/806</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Expressjs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#entry806comment</comments>
      <pubDate>Mon, 9 Mar 2026 10:00:15 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 HTTP 클라이언트 만들기</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTTP 클라이언트란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 HTTP 클라이언트는 외부 API를 호출하거나 웹 페이지를 가져오는 데 사용됩니다. 내장 http/https 모듈을 사용하거나, axios, node-fetch 같은 라이브러리를 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 내장 http/https 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 http.get - 간단한 GET 요청&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const https = require('https');

https.get('https://api.github.com/users/octocat', {
  headers: { 'User-Agent': 'Node.js' }
}, (res) =&amp;gt; {
  let data = '';

  res.on('data', (chunk) =&amp;gt; {
    data += chunk;
  });

  res.on('end', () =&amp;gt; {
    const user = JSON.parse(data);
    console.log('사용자:', user.login);
  });
}).on('error', (err) =&amp;gt; {
  console.error('오류:', err);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 http.request - 모든 HTTP 메서드&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const https = require('https');

const options = {
  hostname: 'jsonplaceholder.typicode.com',
  port: 443,
  path: '/posts',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'User-Agent': 'Node.js'
  }
};

const req = https.request(options, (res) =&amp;gt; {
  console.log('상태 코드:', res.statusCode);
  console.log('헤더:', res.headers);

  let data = '';
  res.on('data', (chunk) =&amp;gt; data += chunk);
  res.on('end', () =&amp;gt; {
    console.log('응답:', JSON.parse(data));
  });
});

req.on('error', (err) =&amp;gt; {
  console.error('요청 오류:', err);
});

// 요청 본문 전송
req.write(JSON.stringify({
  title: '새 글',
  body: '내용입니다',
  userId: 1
}));

req.end();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Promise로 래핑&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const https = require('https');

function httpRequest(options, body = null) {
  return new Promise((resolve, reject) =&amp;gt; {
    const req = https.request(options, (res) =&amp;gt; {
      const chunks = [];

      res.on('data', (chunk) =&amp;gt; chunks.push(chunk));
      res.on('end', () =&amp;gt; {
        const data = Buffer.concat(chunks).toString();
        const response = {
          statusCode: res.statusCode,
          headers: res.headers,
          body: data
        };

        if (res.statusCode &amp;gt;= 200 &amp;amp;&amp;amp; res.statusCode &amp;lt; 300) {
          resolve(response);
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);
    req.on('timeout', () =&amp;gt; {
      req.destroy();
      reject(new Error('Request timeout'));
    });

    if (body) {
      req.write(typeof body === 'string' ? body : JSON.stringify(body));
    }

    req.end();
  });
}

// 사용
async function fetchUser() {
  const response = await httpRequest({
    hostname: 'api.github.com',
    path: '/users/octocat',
    headers: { 'User-Agent': 'Node.js' }
  });

  return JSON.parse(response.body);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. fetch API (Node.js 18+)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 18부터 전역 fetch가 내장되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// GET 요청
const response = await fetch('https://api.github.com/users/octocat', {
  headers: { 'User-Agent': 'Node.js' }
});

const data = await response.json();
console.log(data);

// POST 요청
const postResponse = await fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '새 글',
    body: '내용'
  })
});

const result = await postResponse.json();
console.log(result);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchWithError(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

try {
  const data = await fetchWithError('https://api.example.com/data');
  console.log(data);
} catch (err) {
  console.error('요청 실패:', err.message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 요청 취소&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const controller = new AbortController();
const { signal } = controller;

// 5초 후 취소
setTimeout(() =&amp;gt; controller.abort(), 5000);

try {
  const response = await fetch('https://api.example.com/slow', { signal });
  const data = await response.json();
  console.log(data);
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('요청이 취소되었습니다');
  } else {
    throw err;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 타임아웃 구현&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() =&amp;gt; controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}

const response = await fetchWithTimeout(
  'https://api.example.com/data',
  { headers: { 'Accept': 'application/json' } },
  3000
);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. HTTP 클라이언트 클래스&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class HttpClient {
  constructor(baseURL, defaultHeaders = {}) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders
    };
  }

  async request(path, options = {}) {
    const url = `${this.baseURL}${path}`;
    const config = {
      ...options,
      headers: {
        ...this.defaultHeaders,
        ...options.headers
      }
    };

    if (options.body &amp;amp;&amp;amp; typeof options.body === 'object') {
      config.body = JSON.stringify(options.body);
    }

    const response = await fetch(url, config);

    if (!response.ok) {
      const error = new Error(`HTTP ${response.status}`);
      error.status = response.status;
      error.response = response;
      throw error;
    }

    const contentType = response.headers.get('content-type');
    if (contentType?.includes('application/json')) {
      return response.json();
    }

    return response.text();
  }

  get(path, options = {}) {
    return this.request(path, { ...options, method: 'GET' });
  }

  post(path, body, options = {}) {
    return this.request(path, { ...options, method: 'POST', body });
  }

  put(path, body, options = {}) {
    return this.request(path, { ...options, method: 'PUT', body });
  }

  patch(path, body, options = {}) {
    return this.request(path, { ...options, method: 'PATCH', body });
  }

  delete(path, options = {}) {
    return this.request(path, { ...options, method: 'DELETE' });
  }
}

// 사용
const api = new HttpClient('https://jsonplaceholder.typicode.com');

const posts = await api.get('/posts');
const newPost = await api.post('/posts', { title: 'Hello', body: 'World' });
const updated = await api.put('/posts/1', { title: 'Updated' });
await api.delete('/posts/1');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 재시도 로직&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
  for (let i = 0; i &amp;lt; retries; i++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) {
        return response;
      }

      // 5xx 에러는 재시도
      if (response.status &amp;gt;= 500) {
        throw new Error(`Server error: ${response.status}`);
      }

      // 4xx 에러는 재시도하지 않음
      return response;

    } catch (err) {
      const isLastAttempt = i === retries - 1;

      if (isLastAttempt) {
        throw err;
      }

      console.log(`재시도 ${i + 1}/${retries}...`);
      await new Promise(resolve =&amp;gt; setTimeout(resolve, delay * (i + 1)));
    }
  }
}

// 지수 백오프
async function fetchWithExponentialBackoff(url, options = {}, maxRetries = 5) {
  for (let i = 0; i &amp;lt; maxRetries; i++) {
    try {
      return await fetch(url, options);
    } catch (err) {
      if (i === maxRetries - 1) throw err;

      const delay = Math.min(1000 * Math.pow(2, i), 30000);
      const jitter = Math.random() * 1000;
      await new Promise(resolve =&amp;gt; setTimeout(resolve, delay + jitter));
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 동시 요청 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Promise.all&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 모든 요청 병렬 실행
const urls = [
  'https://api.example.com/users/1',
  'https://api.example.com/users/2',
  'https://api.example.com/users/3'
];

const responses = await Promise.all(
  urls.map(url =&amp;gt; fetch(url).then(res =&amp;gt; res.json()))
);

console.log(responses);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Promise.allSettled&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 일부 실패해도 모든 결과 수집
const results = await Promise.allSettled(
  urls.map(url =&amp;gt; fetch(url).then(res =&amp;gt; res.json()))
);

results.forEach((result, index) =&amp;gt; {
  if (result.status === 'fulfilled') {
    console.log(`${urls[index]}: 성공`, result.value);
  } else {
    console.log(`${urls[index]}: 실패`, result.reason);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 동시성 제한&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchWithConcurrencyLimit(urls, limit = 5) {
  const results = [];
  const executing = new Set();

  for (const url of urls) {
    const promise = fetch(url)
      .then(res =&amp;gt; res.json())
      .then(data =&amp;gt; {
        executing.delete(promise);
        return data;
      });

    results.push(promise);
    executing.add(promise);

    if (executing.size &amp;gt;= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

const urls = Array.from({ length: 100 }, (_, i) =&amp;gt;
  `https://api.example.com/items/${i}`
);

const data = await fetchWithConcurrencyLimit(urls, 10);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 스트리밍 응답&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function streamResponse(url) {
  const response = await fetch(url);

  if (!response.body) {
    throw new Error('ReadableStream not supported');
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();

    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    console.log('청크:', chunk);
  }
}

// 파일 다운로드
const fs = require('fs');
const { Readable } = require('stream');
const { pipeline } = require('stream/promises');

async function downloadFile(url, destPath) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const fileStream = fs.createWriteStream(destPath);
  await pipeline(Readable.fromWeb(response.body), fileStream);

  console.log('다운로드 완료:', destPath);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 인터셉터 패턴&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class HttpClientWithInterceptors {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  addRequestInterceptor(fn) {
    this.requestInterceptors.push(fn);
  }

  addResponseInterceptor(fn) {
    this.responseInterceptors.push(fn);
  }

  async request(path, options = {}) {
    let config = { url: `${this.baseURL}${path}`, ...options };

    // 요청 인터셉터 실행
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor(config);
    }

    let response = await fetch(config.url, config);

    // 응답 인터셉터 실행
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor(response, config);
    }

    return response;
  }
}

// 사용
const client = new HttpClientWithInterceptors('https://api.example.com');

// 요청 로깅
client.addRequestInterceptor(async (config) =&amp;gt; {
  console.log('요청:', config.method || 'GET', config.url);
  return config;
});

// 인증 헤더 추가
client.addRequestInterceptor(async (config) =&amp;gt; {
  config.headers = {
    ...config.headers,
    Authorization: `Bearer ${getToken()}`
  };
  return config;
});

// 응답 로깅
client.addResponseInterceptor(async (response, config) =&amp;gt; {
  console.log('응답:', response.status, config.url);
  return response;
});

// 토큰 갱신
client.addResponseInterceptor(async (response, config) =&amp;gt; {
  if (response.status === 401) {
    await refreshToken();
    return fetch(config.url, config);
  }
  return response;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 캐싱&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class CachedHttpClient {
  constructor(baseURL, ttl = 60000) {
    this.baseURL = baseURL;
    this.cache = new Map();
    this.ttl = ttl;
  }

  getCacheKey(path, options) {
    return `${options.method || 'GET'}:${path}`;
  }

  async get(path, options = {}) {
    const cacheKey = this.getCacheKey(path, options);
    const cached = this.cache.get(cacheKey);

    if (cached &amp;amp;&amp;amp; Date.now() &amp;lt; cached.expiry) {
      console.log('캐시 히트:', path);
      return cached.data;
    }

    const response = await fetch(`${this.baseURL}${path}`, options);
    const data = await response.json();

    this.cache.set(cacheKey, {
      data,
      expiry: Date.now() + this.ttl
    });

    return data;
  }

  invalidate(path) {
    for (const key of this.cache.keys()) {
      if (key.includes(path)) {
        this.cache.delete(key);
      }
    }
  }

  clearCache() {
    this.cache.clear();
  }
}

const cachedClient = new CachedHttpClient('https://api.example.com', 30000);
const data1 = await cachedClient.get('/users');  // 네트워크 요청
const data2 = await cachedClient.get('/users');  // 캐시에서 반환&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 HTTP 클라이언트는 내장 http/https 모듈, Node.js 18+의 fetch API, 또는 axios 같은 라이브러리로 구현할 수 있습니다. 프로덕션 환경에서는 타임아웃, 재시도, 에러 처리, 동시성 제한 등을 구현해야 합니다. fetch API가 Node.js에 내장되면서 별도 라이브러리 없이도 강력한 HTTP 클라이언트를 구축할 수 있게 되었습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/805</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry805comment</comments>
      <pubDate>Sun, 8 Mar 2026 19:00:43 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 Koa.js 프레임워크</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Koajs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Koa.js란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koa.js는 Express.js 팀이 만든 차세대 웹 프레임워크입니다. async/await을 기본으로 사용하며, 더 작고 표현력 있는 미들웨어 구조를 제공합니다. Express보다 가벼우며 내장 기능이 적어 필요한 것만 선택적으로 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install koa&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) =&amp;gt; {
  ctx.body = 'Hello Koa!';
});

app.listen(3000);
console.log('서버 실행 중: http://localhost:3000');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 컨텍스트(Context)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koa는 요청과 응답을 하나의 ctx 객체로 캡슐화합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 ctx 객체&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) =&amp;gt; {
  // 요청 정보
  console.log(ctx.method);        // GET
  console.log(ctx.url);           // /users?page=1
  console.log(ctx.path);          // /users
  console.log(ctx.query);         // { page: '1' }
  console.log(ctx.host);          // localhost:3000
  console.log(ctx.protocol);      // http
  console.log(ctx.ip);            // 127.0.0.1
  console.log(ctx.headers);       // 요청 헤더

  // 응답 설정
  ctx.status = 200;
  ctx.type = 'application/json';
  ctx.body = { message: 'Hello' };

  // 헤더 설정
  ctx.set('X-Custom-Header', 'value');

  // 축약 접근
  ctx.request;   // Koa Request 객체
  ctx.response;  // Koa Response 객체
  ctx.req;       // Node.js request
  ctx.res;       // Node.js response
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 요청 객체&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;app.use(async (ctx) =&amp;gt; {
  const { request } = ctx;

  // 요청 정보
  console.log(request.method);
  console.log(request.url);
  console.log(request.header);
  console.log(request.query);
  console.log(request.querystring);

  // 헤더 접근
  console.log(request.get('User-Agent'));

  // Content-Type 확인
  console.log(request.is('json'));
  console.log(request.is('html'));

  ctx.body = 'OK';
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 응답 객체&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;app.use(async (ctx) =&amp;gt; {
  const { response } = ctx;

  // 상태 코드
  response.status = 201;

  // 응답 본문
  response.body = { created: true };

  // 헤더
  response.set('Cache-Control', 'no-cache');
  response.type = 'application/json';

  // 리다이렉트
  // response.redirect('/other');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 미들웨어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 미들웨어 기본&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koa의 미들웨어는 양파 모델(Onion Model)을 따릅니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const app = new Koa();

// 첫 번째 미들웨어
app.use(async (ctx, next) =&amp;gt; {
  console.log('1. 요청 시작');
  await next();  // 다음 미들웨어 실행
  console.log('6. 요청 종료');
});

// 두 번째 미들웨어
app.use(async (ctx, next) =&amp;gt; {
  console.log('2. 처리 전');
  await next();
  console.log('5. 처리 후');
});

// 세 번째 미들웨어
app.use(async (ctx) =&amp;gt; {
  console.log('3. 핸들러 시작');
  ctx.body = 'Hello';
  console.log('4. 핸들러 종료');
});

// 실행 순서: 1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4 &amp;rarr; 5 &amp;rarr; 6

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 로깅 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;app.use(async (ctx, next) =&amp;gt; {
  const start = Date.now();

  await next();

  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 에러 처리 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// 에러 핸들러 (첫 번째에 위치)
app.use(async (ctx, next) =&amp;gt; {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = {
      error: err.message,
      ...(process.env.NODE_ENV === 'development' &amp;amp;&amp;amp; { stack: err.stack })
    };
    ctx.app.emit('error', err, ctx);
  }
});

// 에러 이벤트 리스너
app.on('error', (err, ctx) =&amp;gt; {
  console.error('서버 에러:', err);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 조건부 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;// 특정 경로에만 적용
function unless(paths, middleware) {
  return async (ctx, next) =&amp;gt; {
    if (paths.includes(ctx.path)) {
      return next();
    }
    return middleware(ctx, next);
  };
}

const authMiddleware = async (ctx, next) =&amp;gt; {
  if (!ctx.headers.authorization) {
    ctx.throw(401, 'Unauthorized');
  }
  await next();
};

app.use(unless(['/login', '/register'], authMiddleware));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 라우팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koa는 라우터가 내장되어 있지 않아 @koa/router를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm install @koa/router&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx) =&amp;gt; {
  ctx.body = 'Home';
});

router.get('/users', (ctx) =&amp;gt; {
  ctx.body = [{ id: 1, name: '홍길동' }];
});

router.get('/users/:id', (ctx) =&amp;gt; {
  ctx.body = { id: ctx.params.id, name: '홍길동' };
});

router.post('/users', (ctx) =&amp;gt; {
  ctx.status = 201;
  ctx.body = { id: 1, ...ctx.request.body };
});

router.put('/users/:id', (ctx) =&amp;gt; {
  ctx.body = { id: ctx.params.id, ...ctx.request.body };
});

router.delete('/users/:id', (ctx) =&amp;gt; {
  ctx.status = 204;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 라우터 프리픽스와 중첩&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();

// API 라우터
const apiRouter = new Router({ prefix: '/api' });

// 사용자 라우터
const userRouter = new Router({ prefix: '/users' });
userRouter.get('/', (ctx) =&amp;gt; { ctx.body = []; });
userRouter.get('/:id', (ctx) =&amp;gt; { ctx.body = {}; });

// 게시글 라우터
const postRouter = new Router({ prefix: '/posts' });
postRouter.get('/', (ctx) =&amp;gt; { ctx.body = []; });

// 라우터 중첩
apiRouter.use(userRouter.routes(), userRouter.allowedMethods());
apiRouter.use(postRouter.routes(), postRouter.allowedMethods());

app.use(apiRouter.routes());
app.use(apiRouter.allowedMethods());

// 결과: /api/users, /api/users/:id, /api/posts

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 라우트별 미들웨어&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const authenticate = async (ctx, next) =&amp;gt; {
  if (!ctx.headers.authorization) {
    ctx.throw(401);
  }
  await next();
};

router.get('/public', (ctx) =&amp;gt; {
  ctx.body = '공개 페이지';
});

router.get('/private', authenticate, (ctx) =&amp;gt; {
  ctx.body = '비공개 페이지';
});

// 여러 미들웨어 체인
router.post('/users',
  authenticate,
  validateBody,
  async (ctx) =&amp;gt; {
    ctx.body = { created: true };
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 요청 본문 파싱&lt;/h2&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install koa-bodyparser&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
const router = new Router();

app.use(bodyParser());

router.post('/users', (ctx) =&amp;gt; {
  const { name, email } = ctx.request.body;
  ctx.body = { name, email };
});

app.use(router.routes());
app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정적 파일 서빙&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install koa-static&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const Koa = require('koa');
const serve = require('koa-static');
const path = require('path');

const app = new Koa();

// public 폴더의 정적 파일 서빙
app.use(serve(path.join(__dirname, 'public')));

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 템플릿 렌더링&lt;/h2&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install koa-views ejs&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const views = require('koa-views');
const path = require('path');

const app = new Koa();

app.use(views(path.join(__dirname, 'views'), {
  extension: 'ejs'
}));

app.use(async (ctx) =&amp;gt; {
  await ctx.render('index', {
    title: 'Koa 애플리케이션',
    users: [{ name: '홍길동' }]
  });
});

app.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실전 API 서버&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');

const app = new Koa();
const router = new Router({ prefix: '/api' });

// 데이터 저장소
const users = new Map();
let nextId = 1;

// 미들웨어
app.use(cors());
app.use(bodyParser());

// 로깅
app.use(async (ctx, next) =&amp;gt; {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
});

// 에러 핸들러
app.use(async (ctx, next) =&amp;gt; {
  try {
    await next();
    if (ctx.status === 404) {
      ctx.throw(404, 'Not Found');
    }
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { error: err.message };
  }
});

// 라우트
router.get('/users', (ctx) =&amp;gt; {
  ctx.body = Array.from(users.values());
});

router.get('/users/:id', (ctx) =&amp;gt; {
  const user = users.get(Number(ctx.params.id));
  if (!user) {
    ctx.throw(404, 'User not found');
  }
  ctx.body = user;
});

router.post('/users', (ctx) =&amp;gt; {
  const { name, email } = ctx.request.body;
  if (!name || !email) {
    ctx.throw(400, 'Name and email required');
  }
  const user = { id: nextId++, name, email };
  users.set(user.id, user);
  ctx.status = 201;
  ctx.body = user;
});

router.put('/users/:id', (ctx) =&amp;gt; {
  const id = Number(ctx.params.id);
  if (!users.has(id)) {
    ctx.throw(404, 'User not found');
  }
  const user = { ...users.get(id), ...ctx.request.body, id };
  users.set(id, user);
  ctx.body = user;
});

router.delete('/users/:id', (ctx) =&amp;gt; {
  const id = Number(ctx.params.id);
  if (!users.delete(id)) {
    ctx.throw(404, 'User not found');
  }
  ctx.status = 204;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000, () =&amp;gt; {
  console.log('Koa API 서버 실행 중');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Koa vs Express&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;Koa&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;미들웨어&lt;/td&gt;
&lt;td&gt;async/await 기반&lt;/td&gt;
&lt;td&gt;콜백 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내장 기능&lt;/td&gt;
&lt;td&gt;최소한&lt;/td&gt;
&lt;td&gt;라우터, 정적 파일 등 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;번들 크기&lt;/td&gt;
&lt;td&gt;더 작음&lt;/td&gt;
&lt;td&gt;더 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러 처리&lt;/td&gt;
&lt;td&gt;try/catch로 간편&lt;/td&gt;
&lt;td&gt;next(err) 패턴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컨텍스트&lt;/td&gt;
&lt;td&gt;ctx 객체&lt;/td&gt;
&lt;td&gt;req, res 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Koa.js는 async/await을 기본으로 사용하는 모던한 웹 프레임워크입니다. 양파 모델의 미들웨어 구조로 요청/응답 흐름을 직관적으로 제어할 수 있고, ctx 객체로 요청과 응답을 통합 관리합니다. Express보다 가볍고 유연하지만, 라우터와 바디 파서 같은 기본 기능도 별도 패키지로 설치해야 합니다. 모던 JavaScript를 선호하고 미니멀한 프레임워크를 원한다면 Koa가 좋은 선택입니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/807</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-Koajs-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC#entry807comment</comments>
      <pubDate>Sun, 8 Mar 2026 18:00:33 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 HTTP 서버 만들기</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTTP 모듈 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 http 모듈은 HTTP 서버와 클라이언트를 생성하기 위한 내장 모듈입니다. 별도의 패키지 설치 없이 웹 서버를 구축할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, World!');
});

server.listen(3000, () =&amp;gt; {
  console.log('서버가 http://localhost:3000 에서 실행 중');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서버 생성 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 createServer 사용&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');

// 방법 1: 콜백 함수 전달
const server1 = http.createServer((req, res) =&amp;gt; {
  res.end('Hello');
});

// 방법 2: request 이벤트 리스너
const server2 = http.createServer();
server2.on('request', (req, res) =&amp;gt; {
  res.end('Hello');
});

server1.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 서버 옵션&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer({
  maxHeaderSize: 8192,           // 헤더 최대 크기
  keepAlive: true,               // Keep-Alive 활성화
  keepAliveInitialDelay: 0,      // Keep-Alive 초기 지연
  requestTimeout: 300000,        // 요청 타임아웃 (5분)
  headersTimeout: 60000,         // 헤더 타임아웃 (1분)
  connectionsCheckingInterval: 30000  // 연결 체크 간격
}, (req, res) =&amp;gt; {
  res.end('Hello');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 요청(Request) 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 요청 정보 읽기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  console.log('메서드:', req.method);          // GET, POST, PUT, DELETE 등
  console.log('URL:', req.url);               // /path?query=value
  console.log('HTTP 버전:', req.httpVersion); // 1.1
  console.log('헤더:', req.headers);          // { host: 'localhost', ... }

  // 특정 헤더 읽기
  console.log('Host:', req.headers.host);
  console.log('User-Agent:', req.headers['user-agent']);

  res.end('Request received');
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 URL 파싱&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const http = require('http');
const url = require('url');

const server = http.createServer((req, res) =&amp;gt; {
  // URL 파싱
  const parsedUrl = new URL(req.url, `http://${req.headers.host}`);

  console.log('경로:', parsedUrl.pathname);           // /users
  console.log('쿼리 문자열:', parsedUrl.search);      // ?id=123
  console.log('쿼리 파라미터:', parsedUrl.searchParams.get('id'));  // 123

  res.end('URL parsed');
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 요청 본문 읽기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  if (req.method === 'POST') {
    let body = '';

    req.on('data', (chunk) =&amp;gt; {
      body += chunk.toString();
    });

    req.on('end', () =&amp;gt; {
      console.log('본문:', body);

      // JSON 파싱
      try {
        const data = JSON.parse(body);
        console.log('파싱된 데이터:', data);
      } catch (e) {
        console.log('JSON이 아닌 데이터');
      }

      res.end('Body received');
    });
  } else {
    res.end('Send POST request');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Promise 기반 본문 읽기&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const http = require('http');

function getRequestBody(req) {
  return new Promise((resolve, reject) =&amp;gt; {
    const chunks = [];

    req.on('data', (chunk) =&amp;gt; chunks.push(chunk));
    req.on('end', () =&amp;gt; resolve(Buffer.concat(chunks).toString()));
    req.on('error', reject);
  });
}

const server = http.createServer(async (req, res) =&amp;gt; {
  if (req.method === 'POST') {
    const body = await getRequestBody(req);
    const data = JSON.parse(body);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ received: data }));
  } else {
    res.end('Hello');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 응답(Response) 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 상태 코드와 헤더 설정&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  // 상태 코드와 헤더 한 번에 설정
  res.writeHead(200, {
    'Content-Type': 'text/html; charset=utf-8',
    'Cache-Control': 'no-cache',
    'X-Custom-Header': 'value'
  });

  res.end('&amp;lt;h1&amp;gt;안녕하세요&amp;lt;/h1&amp;gt;');
});

// 또는 개별 설정
const server2 = http.createServer((req, res) =&amp;gt; {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('X-Powered-By', 'Node.js');

  res.end(JSON.stringify({ message: 'Hello' }));
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 다양한 응답 타입&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) =&amp;gt; {
  const path = req.url;

  if (path === '/text') {
    // 텍스트 응답
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end('일반 텍스트');
  }
  else if (path === '/html') {
    // HTML 응답
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end('&amp;lt;h1&amp;gt;HTML 페이지&amp;lt;/h1&amp;gt;');
  }
  else if (path === '/json') {
    // JSON 응답
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ name: '홍길동', age: 30 }));
  }
  else if (path === '/image') {
    // 이미지 응답
    res.writeHead(200, { 'Content-Type': 'image/png' });
    fs.createReadStream('image.png').pipe(res);
  }
  else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 청크 응답&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.writeHead(200, {
    'Content-Type': 'text/plain',
    'Transfer-Encoding': 'chunked'
  });

  // 여러 번에 걸쳐 응답
  res.write('첫 번째 청크\n');

  setTimeout(() =&amp;gt; {
    res.write('두 번째 청크\n');
  }, 1000);

  setTimeout(() =&amp;gt; {
    res.write('세 번째 청크\n');
    res.end();  // 응답 종료
  }, 2000);
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 라우팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  const method = req.method;
  const url = new URL(req.url, `http://${req.headers.host}`);
  const path = url.pathname;

  // 라우팅
  if (method === 'GET' &amp;amp;&amp;amp; path === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('&amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;');
  }
  else if (method === 'GET' &amp;amp;&amp;amp; path === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('&amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;');
  }
  else if (method === 'GET' &amp;amp;&amp;amp; path.startsWith('/users/')) {
    const userId = path.split('/')[2];
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ userId }));
  }
  else if (method === 'POST' &amp;amp;&amp;amp; path === '/users') {
    // POST 처리
    res.writeHead(201, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'User created' }));
  }
  else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 라우터 클래스&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const http = require('http');

class Router {
  constructor() {
    this.routes = [];
  }

  add(method, path, handler) {
    this.routes.push({
      method: method.toUpperCase(),
      path: new RegExp(`^${path.replace(/:\w+/g, '([^/]+)')}$`),
      handler,
      paramNames: (path.match(/:\w+/g) || []).map(p =&amp;gt; p.slice(1))
    });
  }

  get(path, handler) { this.add('GET', path, handler); }
  post(path, handler) { this.add('POST', path, handler); }
  put(path, handler) { this.add('PUT', path, handler); }
  delete(path, handler) { this.add('DELETE', path, handler); }

  handle(req, res) {
    const url = new URL(req.url, `http://${req.headers.host}`);

    for (const route of this.routes) {
      if (route.method !== req.method) continue;

      const match = url.pathname.match(route.path);
      if (match) {
        req.params = {};
        route.paramNames.forEach((name, i) =&amp;gt; {
          req.params[name] = match[i + 1];
        });
        req.query = Object.fromEntries(url.searchParams);

        return route.handler(req, res);
      }
    }

    res.writeHead(404);
    res.end('Not Found');
  }
}

// 사용
const router = new Router();

router.get('/', (req, res) =&amp;gt; {
  res.end('Home');
});

router.get('/users/:id', (req, res) =&amp;gt; {
  res.end(`User ID: ${req.params.id}`);
});

router.post('/users', (req, res) =&amp;gt; {
  res.writeHead(201);
  res.end('Created');
});

const server = http.createServer((req, res) =&amp;gt; router.handle(req, res));
server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정적 파일 서빙&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');
const fs = require('fs');
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',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon'
};

const server = http.createServer((req, res) =&amp;gt; {
  // URL에서 파일 경로 추출
  let filePath = path.join(__dirname, 'public', req.url);

  // 디렉토리면 index.html 추가
  if (req.url === '/') {
    filePath = path.join(__dirname, 'public', 'index.html');
  }

  // 파일 확장자로 MIME 타입 결정
  const ext = path.extname(filePath);
  const contentType = MIME_TYPES[ext] || 'application/octet-stream';

  // 파일 읽기
  fs.readFile(filePath, (err, content) =&amp;gt; {
    if (err) {
      if (err.code === 'ENOENT') {
        res.writeHead(404);
        res.end('File Not Found');
      } else {
        res.writeHead(500);
        res.end('Server Error');
      }
      return;
    }

    res.writeHead(200, { 'Content-Type': contentType });
    res.end(content);
  });
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 서버 이벤트&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.end('Hello');
});

// 서버 이벤트
server.on('listening', () =&amp;gt; {
  console.log('서버 시작됨');
});

server.on('connection', (socket) =&amp;gt; {
  console.log('새 연결:', socket.remoteAddress);
});

server.on('close', () =&amp;gt; {
  console.log('서버 종료됨');
});

server.on('error', (err) =&amp;gt; {
  if (err.code === 'EADDRINUSE') {
    console.error('포트가 이미 사용 중입니다');
  } else {
    console.error('서버 오류:', err);
  }
});

server.on('clientError', (err, socket) =&amp;gt; {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 서버 제어&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.end('Hello');
});

// 서버 시작
server.listen(3000, '0.0.0.0', () =&amp;gt; {
  console.log('서버 주소:', server.address());
  // { address: '0.0.0.0', family: 'IPv4', port: 3000 }
});

// 서버 종료 (기존 연결은 유지)
setTimeout(() =&amp;gt; {
  server.close(() =&amp;gt; {
    console.log('서버 종료됨');
  });
}, 60000);

// 새 연결 거부, 기존 연결 즉시 종료
process.on('SIGTERM', () =&amp;gt; {
  server.close(() =&amp;gt; {
    process.exit(0);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실전 예시: 간단한 API 서버&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const http = require('http');

// 데이터 저장소
const users = new Map();
let nextId = 1;

// 요청 본문 파싱
async function parseBody(req) {
  const chunks = [];
  for await (const chunk of req) {
    chunks.push(chunk);
  }
  const body = Buffer.concat(chunks).toString();
  return body ? JSON.parse(body) : null;
}

// JSON 응답
function jsonResponse(res, statusCode, data) {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

// 라우트 핸들러
const handlers = {
  'GET /users': (req, res) =&amp;gt; {
    jsonResponse(res, 200, Array.from(users.values()));
  },

  'GET /users/:id': (req, res, params) =&amp;gt; {
    const user = users.get(parseInt(params.id));
    if (user) {
      jsonResponse(res, 200, user);
    } else {
      jsonResponse(res, 404, { error: 'User not found' });
    }
  },

  'POST /users': async (req, res) =&amp;gt; {
    const body = await parseBody(req);
    const user = { id: nextId++, ...body };
    users.set(user.id, user);
    jsonResponse(res, 201, user);
  },

  'PUT /users/:id': async (req, res, params) =&amp;gt; {
    const id = parseInt(params.id);
    if (!users.has(id)) {
      return jsonResponse(res, 404, { error: 'User not found' });
    }
    const body = await parseBody(req);
    const user = { id, ...body };
    users.set(id, user);
    jsonResponse(res, 200, user);
  },

  'DELETE /users/:id': (req, res, params) =&amp;gt; {
    const id = parseInt(params.id);
    if (users.delete(id)) {
      jsonResponse(res, 204, null);
    } else {
      jsonResponse(res, 404, { error: 'User not found' });
    }
  }
};

// 라우팅
function route(req, res) {
  const url = new URL(req.url, `http://${req.headers.host}`);

  for (const [pattern, handler] of Object.entries(handlers)) {
    const [method, path] = pattern.split(' ');
    if (method !== req.method) continue;

    const regex = new RegExp(`^${path.replace(/:(\w+)/g, '(?&amp;lt;$1&amp;gt;[^/]+)')}$`);
    const match = url.pathname.match(regex);

    if (match) {
      return handler(req, res, match.groups || {});
    }
  }

  jsonResponse(res, 404, { error: 'Not found' });
}

const server = http.createServer(route);
server.listen(3000, () =&amp;gt; {
  console.log('API 서버 실행 중: http://localhost:3000');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 http 모듈로 HTTP 서버를 직접 구현할 수 있습니다. createServer로 서버를 생성하고, 요청 객체(req)에서 메서드, URL, 헤더, 본문을 읽고, 응답 객체(res)로 상태 코드, 헤더, 본문을 전송합니다. 실제 프로덕션에서는 Express.js 같은 프레임워크를 사용하지만, 기본 http 모듈의 동작 원리를 이해하면 문제 해결과 최적화에 도움이 됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/804</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry804comment</comments>
      <pubDate>Sun, 8 Mar 2026 08:00:37 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 파일 스트림 처리</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%B2%98%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;#&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 파일 스트림이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 스트림은 대용량 파일을 메모리에 한 번에 로드하지 않고 작은 청크(chunk) 단위로 읽거나 쓰는 방식입니다. 메모리 효율적이며, 파일 크기에 관계없이 일정한 메모리만 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs');

// 일반 방식: 전체 파일을 메모리에 로드
const data = fs.readFileSync('large-file.txt');  // 메모리 부족 가능

// 스트림 방식: 청크 단위로 처리
const stream = fs.createReadStream('large-file.txt');  // 메모리 효율적&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 읽기 스트림 (Readable Stream)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 createReadStream 기본 사용&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('large-file.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024  // 청크 크기 (기본 64KB)
});

readStream.on('data', (chunk) =&amp;gt; {
  console.log('청크 크기:', chunk.length);
  console.log('내용:', chunk);
});

readStream.on('end', () =&amp;gt; {
  console.log('읽기 완료');
});

readStream.on('error', (err) =&amp;gt; {
  console.error('오류:', err);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 옵션 설정&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('file.txt', {
  encoding: 'utf8',      // 문자 인코딩
  highWaterMark: 16384,  // 청크 크기 (바이트)
  start: 0,              // 시작 위치
  end: 999,              // 끝 위치 (포함)
  autoClose: true,       // 완료 시 자동 닫기
  emitClose: true        // close 이벤트 발생
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 일시정지와 재개&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('large-file.txt');

readStream.on('data', (chunk) =&amp;gt; {
  console.log('청크 수신');

  // 처리가 느린 경우 일시정지
  readStream.pause();

  // 비동기 작업 후 재개
  processChunk(chunk).then(() =&amp;gt; {
    readStream.resume();
  });
});

async function processChunk(chunk) {
  // 청크 처리 로직
  await new Promise(resolve =&amp;gt; setTimeout(resolve, 100));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 비동기 이터레이터 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

async function readFileByChunks(filepath) {
  const readStream = fs.createReadStream(filepath, {
    encoding: 'utf8',
    highWaterMark: 1024
  });

  for await (const chunk of readStream) {
    console.log('청크:', chunk.length, '바이트');
    // 청크 처리
  }

  console.log('완료');
}

readFileByChunks('large-file.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 쓰기 스트림 (Writable Stream)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 createWriteStream 기본 사용&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt', {
  encoding: 'utf8'
});

writeStream.write('첫 번째 줄\n');
writeStream.write('두 번째 줄\n');
writeStream.write('세 번째 줄\n');

// 스트림 종료
writeStream.end('마지막 줄');

writeStream.on('finish', () =&amp;gt; {
  console.log('쓰기 완료');
});

writeStream.on('error', (err) =&amp;gt; {
  console.error('오류:', err);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 옵션 설정&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt', {
  encoding: 'utf8',
  flags: 'w',            // 쓰기 모드 (w: 덮어쓰기, a: 추가)
  mode: 0o644,           // 파일 권한
  highWaterMark: 16384,  // 버퍼 크기
  autoClose: true,       // 자동 닫기
  start: 0               // 시작 위치 (flags가 r+일 때)
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 백프레셔 처리&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt');

function writeData(data) {
  // write()가 false를 반환하면 버퍼가 찼음
  const canContinue = writeStream.write(data);

  if (!canContinue) {
    console.log('버퍼 가득 참, 대기 중...');
    // drain 이벤트를 기다림
  }

  return canContinue;
}

writeStream.on('drain', () =&amp;gt; {
  console.log('버퍼 비워짐, 계속 쓰기 가능');
});

// 대용량 데이터 쓰기
for (let i = 0; i &amp;lt; 1000000; i++) {
  const canContinue = writeData(`Line ${i}\n`);
  if (!canContinue) {
    // 실제로는 drain을 기다려야 함
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Promise로 백프레셔 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

function writeWithBackpressure(stream, data) {
  return new Promise((resolve, reject) =&amp;gt; {
    const canContinue = stream.write(data);

    if (canContinue) {
      resolve();
    } else {
      stream.once('drain', resolve);
      stream.once('error', reject);
    }
  });
}

async function writeLargeData() {
  const writeStream = fs.createWriteStream('large-output.txt');

  for (let i = 0; i &amp;lt; 1000000; i++) {
    await writeWithBackpressure(writeStream, `Line ${i}\n`);
  }

  writeStream.end();
}

writeLargeData();&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파이프 (Pipe)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 기본 파이프&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('dest.txt');

// 읽기 스트림을 쓰기 스트림에 연결
readStream.pipe(writeStream);

writeStream.on('finish', () =&amp;gt; {
  console.log('복사 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 여러 스트림 연결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs');
const zlib = require('zlib');

// 파일 읽기 &amp;rarr; 압축 &amp;rarr; 파일 쓰기
fs.createReadStream('file.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('file.txt.gz'))
  .on('finish', () =&amp;gt; {
    console.log('압축 완료');
  });

// 압축 해제
fs.createReadStream('file.txt.gz')
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream('file-unzipped.txt'));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 pipeline 사용 (권장)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');
const { pipeline } = require('stream');
const zlib = require('zlib');

// 콜백 방식
pipeline(
  fs.createReadStream('input.txt'),
  zlib.createGzip(),
  fs.createWriteStream('input.txt.gz'),
  (err) =&amp;gt; {
    if (err) {
      console.error('파이프라인 오류:', err);
    } else {
      console.log('완료');
    }
  }
);

// Promise 방식
const { pipeline: pipelineAsync } = require('stream/promises');

async function compressFile() {
  await pipelineAsync(
    fs.createReadStream('input.txt'),
    zlib.createGzip(),
    fs.createWriteStream('input.txt.gz')
  );
  console.log('압축 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Transform 스트림&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 내장 Transform 스트림&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');
const { Transform } = require('stream');

// 대문자 변환 스트림
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

fs.createReadStream('input.txt')
  .pipe(upperCaseTransform)
  .pipe(fs.createWriteStream('output.txt'));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 커스텀 Transform 클래스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { Transform } = require('stream');

class LineNumberTransform extends Transform {
  constructor(options) {
    super(options);
    this.lineNumber = 1;
    this.buffer = '';
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split('\n');

    // 마지막 줄은 불완전할 수 있으므로 버퍼에 유지
    this.buffer = lines.pop();

    for (const line of lines) {
      this.push(`${this.lineNumber++}: ${line}\n`);
    }

    callback();
  }

  _flush(callback) {
    // 남은 버퍼 처리
    if (this.buffer) {
      this.push(`${this.lineNumber}: ${this.buffer}\n`);
    }
    callback();
  }
}

const fs = require('fs');
const { pipeline } = require('stream/promises');

async function addLineNumbers() {
  await pipeline(
    fs.createReadStream('input.txt'),
    new LineNumberTransform(),
    fs.createWriteStream('numbered.txt')
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실전 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 대용량 파일 복사&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');
const { pipeline } = require('stream/promises');

async function copyFile(source, dest, onProgress) {
  const sourceStats = await fs.promises.stat(source);
  const totalBytes = sourceStats.size;
  let copiedBytes = 0;

  const progressStream = new (require('stream').Transform)({
    transform(chunk, encoding, callback) {
      copiedBytes += chunk.length;
      if (onProgress) {
        onProgress(copiedBytes, totalBytes);
      }
      callback(null, chunk);
    }
  });

  await pipeline(
    fs.createReadStream(source),
    progressStream,
    fs.createWriteStream(dest)
  );
}

await copyFile('large-file.bin', 'copy.bin', (copied, total) =&amp;gt; {
  const percent = ((copied / total) * 100).toFixed(2);
  console.log(`진행률: ${percent}%`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 CSV 파일 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');
const { Transform } = require('stream');
const { pipeline } = require('stream/promises');

class CSVParser extends Transform {
  constructor(options = {}) {
    super({ objectMode: true });
    this.headers = null;
    this.buffer = '';
    this.delimiter = options.delimiter || ',';
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split('\n');
    this.buffer = lines.pop();

    for (const line of lines) {
      if (!line.trim()) continue;

      const values = line.split(this.delimiter);

      if (!this.headers) {
        this.headers = values;
      } else {
        const obj = {};
        this.headers.forEach((header, i) =&amp;gt; {
          obj[header.trim()] = values[i]?.trim();
        });
        this.push(obj);
      }
    }

    callback();
  }
}

async function processCSV(filepath) {
  const readStream = fs.createReadStream(filepath);
  const parser = new CSVParser();

  for await (const row of readStream.pipe(parser)) {
    console.log(row);
    // { name: '홍길동', age: '30', city: '서울' }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 로그 파일 테일&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

function tailFile(filepath, onLine) {
  let position = 0;
  let buffer = '';

  // 파일 변경 감시
  fs.watchFile(filepath, { interval: 100 }, async (curr, prev) =&amp;gt; {
    if (curr.size &amp;gt; prev.size) {
      const stream = fs.createReadStream(filepath, {
        start: position,
        encoding: 'utf8'
      });

      stream.on('data', (chunk) =&amp;gt; {
        buffer += chunk;
        const lines = buffer.split('\n');
        buffer = lines.pop();

        for (const line of lines) {
          if (line.trim()) {
            onLine(line);
          }
        }
      });

      stream.on('end', () =&amp;gt; {
        position = curr.size;
      });
    }
  });

  return () =&amp;gt; fs.unwatchFile(filepath);
}

// 사용
const stopTail = tailFile('./app.log', (line) =&amp;gt; {
  console.log('새 로그:', line);
});

// 종료 시
// stopTail();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 파일 암호화/복호화&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs');
const crypto = require('crypto');
const { pipeline } = require('stream/promises');

async function encryptFile(source, dest, password) {
  const key = crypto.scryptSync(password, 'salt', 32);
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

  // IV를 파일 시작 부분에 저장
  const destStream = fs.createWriteStream(dest);
  destStream.write(iv);

  await pipeline(
    fs.createReadStream(source),
    cipher,
    destStream
  );
}

async function decryptFile(source, dest, password) {
  const key = crypto.scryptSync(password, 'salt', 32);

  // IV 읽기
  const fd = await fs.promises.open(source, 'r');
  const ivBuffer = Buffer.alloc(16);
  await fd.read(ivBuffer, 0, 16, 0);
  await fd.close();

  const decipher = crypto.createDecipheriv('aes-256-cbc', key, ivBuffer);

  await pipeline(
    fs.createReadStream(source, { start: 16 }),
    decipher,
    fs.createWriteStream(dest)
  );
}

await encryptFile('secret.txt', 'secret.enc', 'mypassword');
await decryptFile('secret.enc', 'decrypted.txt', 'mypassword');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 스트림 유틸리티&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 finished - 스트림 완료 감지&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { finished } = require('stream/promises');
const fs = require('fs');

async function waitForStream() {
  const writeStream = fs.createWriteStream('output.txt');

  writeStream.write('데이터 1\n');
  writeStream.write('데이터 2\n');
  writeStream.end();

  await finished(writeStream);
  console.log('스트림 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 Readable.from - 이터러블을 스트림으로&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { Readable } = require('stream');
const fs = require('fs');

// 배열을 스트림으로
const arrayStream = Readable.from(['Hello', ' ', 'World']);

// 제너레이터를 스트림으로
async function* generateData() {
  for (let i = 0; i &amp;lt; 100; i++) {
    yield `Line ${i}\n`;
  }
}

const generatorStream = Readable.from(generateData());
generatorStream.pipe(fs.createWriteStream('generated.txt'));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 파일 스트림은 대용량 파일을 메모리 효율적으로 처리하는 핵심 기능입니다. createReadStream과 createWriteStream으로 스트림을 생성하고, pipe나 pipeline으로 연결합니다. Transform 스트림을 사용하면 데이터를 변환하면서 처리할 수 있습니다. 백프레셔를 적절히 처리하고, pipeline을 사용하면 에러 처리도 자동으로 됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/803</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%B2%98%EB%A6%AC#entry803comment</comments>
      <pubDate>Sat, 7 Mar 2026 22:00:56 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 디렉토리 생성 및 삭제</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%82%AD%EC%A0%9C</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 디렉토리 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 mkdir - 디렉토리 생성&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 콜백 방식
fs.mkdir('new-folder', (err) =&amp;gt; {
  if (err) {
    console.error('디렉토리 생성 오류:', err);
    return;
  }
  console.log('디렉토리가 생성되었습니다.');
});

// 동기 방식
try {
  fs.mkdirSync('new-folder');
  console.log('디렉토리가 생성되었습니다.');
} catch (err) {
  console.error('오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Promise 기반 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function createDirectory(dirPath) {
  try {
    await fs.mkdir(dirPath);
    console.log('디렉토리가 생성되었습니다.');
  } catch (err) {
    if (err.code === 'EEXIST') {
      console.log('디렉토리가 이미 존재합니다.');
    } else {
      throw err;
    }
  }
}

await createDirectory('my-folder');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 재귀적 생성 (recursive)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩된 디렉토리를 한 번에 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 중첩 디렉토리 생성
await fs.mkdir('path/to/deep/folder', { recursive: true });

// 이미 존재해도 오류 없음
await fs.mkdir('existing/path', { recursive: true });

// 생성된 첫 번째 디렉토리 경로 반환
const created = await fs.mkdir('a/b/c', { recursive: true });
console.log(created);  // 'a' 또는 undefined (이미 존재할 경우)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 권한 지정&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 권한과 함께 생성
await fs.mkdir('secure-folder', {
  recursive: true,
  mode: 0o755  // rwxr-xr-x
});

// 제한적 권한
await fs.mkdir('private-folder', {
  mode: 0o700  // rwx------
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 디렉토리 삭제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 rmdir - 빈 디렉토리 삭제&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 콜백 방식 (빈 디렉토리만)
fs.rmdir('empty-folder', (err) =&amp;gt; {
  if (err) {
    if (err.code === 'ENOTEMPTY') {
      console.log('디렉토리가 비어있지 않습니다.');
    } else {
      console.error('오류:', err);
    }
    return;
  }
  console.log('디렉토리가 삭제되었습니다.');
});

// 동기 방식
try {
  fs.rmdirSync('empty-folder');
} catch (err) {
  console.error('오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 rm - 디렉토리 재귀 삭제 (Node.js 14.14+)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 디렉토리와 내용물 모두 삭제
await fs.rm('folder-with-files', { recursive: true });

// 존재하지 않아도 오류 없이 진행
await fs.rm('maybe-exists', { recursive: true, force: true });

// 콜백 방식
fs.rm('folder', { recursive: true, force: true }, (err) =&amp;gt; {
  if (err) throw err;
  console.log('삭제 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Promise 기반 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function removeDirectory(dirPath, options = {}) {
  const { force = false } = options;

  try {
    await fs.rm(dirPath, { recursive: true, force });
    console.log(`${dirPath} 삭제 완료`);
    return true;
  } catch (err) {
    if (err.code === 'ENOENT' &amp;amp;&amp;amp; force) {
      return true;  // 존재하지 않아도 성공
    }
    throw err;
  }
}

await removeDirectory('old-folder', { force: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 디렉토리 읽기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 readdir - 디렉토리 내용 읽기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 파일/폴더 이름 목록
const files = await fs.readdir('my-folder');
console.log(files);  // ['file1.txt', 'file2.txt', 'subfolder']

// 파일 타입 정보 포함
const entries = await fs.readdir('my-folder', { withFileTypes: true });
for (const entry of entries) {
  if (entry.isFile()) {
    console.log(`파일: ${entry.name}`);
  } else if (entry.isDirectory()) {
    console.log(`폴더: ${entry.name}`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 재귀적 읽기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function readDirRecursive(dir) {
  const result = [];
  const entries = await fs.readdir(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);

    if (entry.isDirectory()) {
      const subFiles = await readDirRecursive(fullPath);
      result.push(...subFiles);
    } else {
      result.push(fullPath);
    }
  }

  return result;
}

const allFiles = await readDirRecursive('./src');
console.log(allFiles);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 recursive 옵션 (Node.js 18.17+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 재귀적으로 모든 파일/폴더 읽기
const allEntries = await fs.readdir('src', { recursive: true });
console.log(allEntries);
// ['file1.js', 'components', 'components/Button.js', ...]&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 디렉토리 존재 확인&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// access 사용
async function directoryExists(dirPath) {
  try {
    await fs.access(dirPath);
    const stats = await fs.stat(dirPath);
    return stats.isDirectory();
  } catch {
    return false;
  }
}

// 사용
if (await directoryExists('my-folder')) {
  console.log('디렉토리가 존재합니다.');
}

// 동기 방식
const exists = fs.existsSync('my-folder') &amp;amp;&amp;amp;
               fs.statSync('my-folder').isDirectory();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 디렉토리 정보 조회&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function getDirectoryInfo(dirPath) {
  const stats = await fs.stat(dirPath);

  return {
    isDirectory: stats.isDirectory(),
    size: stats.size,
    created: stats.birthtime,
    modified: stats.mtime,
    accessed: stats.atime,
    mode: stats.mode.toString(8)
  };
}

const info = await getDirectoryInfo('my-folder');
console.log(info);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 디렉토리 복사&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 cp 사용 (Node.js 16.7+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 디렉토리 전체 복사
await fs.cp('source-folder', 'dest-folder', { recursive: true });

// 옵션 지정
await fs.cp('src', 'dst', {
  recursive: true,
  force: true,  // 기존 파일 덮어쓰기
  preserveTimestamps: true,
  filter: (src) =&amp;gt; !src.includes('node_modules')
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 수동 구현&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function copyDirectory(src, dest) {
  await fs.mkdir(dest, { recursive: true });
  const entries = await fs.readdir(src, { withFileTypes: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    if (entry.isDirectory()) {
      await copyDirectory(srcPath, destPath);
    } else {
      await fs.copyFile(srcPath, destPath);
    }
  }
}

await copyDirectory('source', 'backup');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 임시 디렉토리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 mkdtemp - 임시 디렉토리 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const os = require('os');
const path = require('path');

// 시스템 임시 폴더에 생성
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'myapp-'));
console.log(tempDir);  // /tmp/myapp-abc123

// 작업 수행
// ...

// 정리
await fs.rm(tempDir, { recursive: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 임시 디렉토리 관리 클래스&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const os = require('os');
const path = require('path');

class TempDirectory {
  constructor(prefix = 'tmp') {
    this.prefix = prefix;
    this.path = null;
  }

  async create() {
    this.path = await fs.mkdtemp(
      path.join(os.tmpdir(), `${this.prefix}-`)
    );
    return this.path;
  }

  async cleanup() {
    if (this.path) {
      await fs.rm(this.path, { recursive: true, force: true });
      this.path = null;
    }
  }

  getPath(...subPaths) {
    return path.join(this.path, ...subPaths);
  }
}

// 사용
const temp = new TempDirectory('build');
await temp.create();

try {
  // 임시 폴더에서 작업
  await fs.writeFile(temp.getPath('output.txt'), 'data');
} finally {
  await temp.cleanup();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실전 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 프로젝트 구조 생성기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function createProjectStructure(rootDir, structure) {
  await fs.mkdir(rootDir, { recursive: true });

  for (const [name, content] of Object.entries(structure)) {
    const fullPath = path.join(rootDir, name);

    if (typeof content === 'object') {
      // 디렉토리인 경우 재귀
      await createProjectStructure(fullPath, content);
    } else {
      // 파일인 경우 생성
      await fs.mkdir(path.dirname(fullPath), { recursive: true });
      await fs.writeFile(fullPath, content);
    }
  }
}

// 프로젝트 구조 정의
const structure = {
  'src': {
    'index.js': 'console.log(&quot;Hello&quot;);',
    'utils': {
      'helpers.js': 'module.exports = {};'
    }
  },
  'tests': {
    'index.test.js': 'test(&quot;example&quot;, () =&amp;gt; {});'
  },
  'package.json': '{&quot;name&quot;: &quot;my-project&quot;}',
  'README.md': '# My Project'
};

await createProjectStructure('my-new-project', structure);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 디렉토리 크기 계산&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function getDirectorySize(dirPath) {
  let totalSize = 0;
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);

    if (entry.isDirectory()) {
      totalSize += await getDirectorySize(fullPath);
    } else {
      const stats = await fs.stat(fullPath);
      totalSize += stats.size;
    }
  }

  return totalSize;
}

function formatSize(bytes) {
  const units = ['B', 'KB', 'MB', 'GB'];
  let unitIndex = 0;
  let size = bytes;

  while (size &amp;gt;= 1024 &amp;amp;&amp;amp; unitIndex &amp;lt; units.length - 1) {
    size /= 1024;
    unitIndex++;
  }

  return `${size.toFixed(2)} ${units[unitIndex]}`;
}

const size = await getDirectorySize('./node_modules');
console.log('총 크기:', formatSize(size));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 디렉토리 감시 및 정리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function cleanOldFiles(dirPath, maxAgeDays) {
  const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  let deletedCount = 0;
  let deletedSize = 0;

  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    const stats = await fs.stat(fullPath);

    if (stats.mtimeMs &amp;lt; cutoff) {
      if (entry.isDirectory()) {
        await fs.rm(fullPath, { recursive: true });
      } else {
        deletedSize += stats.size;
        await fs.unlink(fullPath);
      }
      deletedCount++;
    }
  }

  return { deletedCount, deletedSize };
}

// 30일 이상된 파일 정리
const result = await cleanOldFiles('./temp', 30);
console.log(`${result.deletedCount}개 삭제, ${result.deletedSize} bytes 확보`);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.4 디렉토리 동기화&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function syncDirectories(source, target) {
  await fs.mkdir(target, { recursive: true });

  const sourceEntries = await fs.readdir(source, { withFileTypes: true });
  const targetEntries = await fs.readdir(target, { withFileTypes: true });
  const targetNames = new Set(targetEntries.map(e =&amp;gt; e.name));

  // 소스에 있는 파일 복사/업데이트
  for (const entry of sourceEntries) {
    const sourcePath = path.join(source, entry.name);
    const targetPath = path.join(target, entry.name);

    if (entry.isDirectory()) {
      await syncDirectories(sourcePath, targetPath);
    } else {
      const sourceStats = await fs.stat(sourcePath);

      try {
        const targetStats = await fs.stat(targetPath);

        // 소스가 더 최신이면 복사
        if (sourceStats.mtimeMs &amp;gt; targetStats.mtimeMs) {
          await fs.copyFile(sourcePath, targetPath);
        }
      } catch {
        // 타겟에 없으면 복사
        await fs.copyFile(sourcePath, targetPath);
      }
    }

    targetNames.delete(entry.name);
  }

  // 소스에 없는 타겟 파일 삭제
  for (const name of targetNames) {
    const targetPath = path.join(target, name);
    await fs.rm(targetPath, { recursive: true });
  }
}

await syncDirectories('./source', './backup');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 디렉토리 생성은 mkdir, 삭제는 rmdir 또는 rm을 사용합니다. recursive 옵션을 사용하면 중첩된 디렉토리를 한 번에 생성하거나 내용물이 있는 디렉토리를 삭제할 수 있습니다. mkdtemp로 임시 디렉토리를 생성하고, Node.js 16.7+의 cp로 디렉토리를 복사할 수 있습니다. 디렉토리 작업 시에는 경로 처리를 위해 path 모듈과 함께 사용하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/802</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%82%AD%EC%A0%9C#entry802comment</comments>
      <pubDate>Sat, 7 Mar 2026 18:00:01 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 파일 삭제 및 이동</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%9D%B4%EB%8F%99</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 파일 삭제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 unlink - 파일 삭제&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 콜백 방식
fs.unlink('file.txt', (err) =&amp;gt; {
  if (err) {
    console.error('파일 삭제 오류:', err);
    return;
  }
  console.log('파일이 삭제되었습니다.');
});

// 동기 방식
try {
  fs.unlinkSync('file.txt');
  console.log('파일이 삭제되었습니다.');
} catch (err) {
  console.error('파일 삭제 오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Promise 기반 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function deleteFile(filepath) {
  try {
    await fs.unlink(filepath);
    console.log('파일이 삭제되었습니다.');
  } catch (err) {
    if (err.code === 'ENOENT') {
      console.log('파일이 존재하지 않습니다.');
    } else {
      throw err;
    }
  }
}

await deleteFile('example.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 rm - 파일/디렉토리 삭제 (Node.js 14.14+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 파일 삭제
await fs.rm('file.txt');

// 디렉토리 재귀 삭제 (내부 파일 포함)
await fs.rm('folder', { recursive: true });

// 존재하지 않아도 오류 없이 진행
await fs.rm('maybe-exists.txt', { force: true });

// 재귀 + 강제 삭제
await fs.rm('folder', { recursive: true, force: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 파일 이동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 rename - 이름 변경/이동&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 콜백 방식
fs.rename('old.txt', 'new.txt', (err) =&amp;gt; {
  if (err) throw err;
  console.log('파일 이름이 변경되었습니다.');
});

// 다른 디렉토리로 이동
fs.rename('file.txt', 'backup/file.txt', (err) =&amp;gt; {
  if (err) throw err;
  console.log('파일이 이동되었습니다.');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Promise 기반 이동&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function moveFile(source, destination) {
  try {
    await fs.rename(source, destination);
    console.log(`${source} &amp;rarr; ${destination} 이동 완료`);
  } catch (err) {
    if (err.code === 'EXDEV') {
      // 다른 파일 시스템 간 이동 시 복사 후 삭제
      await fs.copyFile(source, destination);
      await fs.unlink(source);
      console.log(`${source} &amp;rarr; ${destination} 이동 완료 (복사 방식)`);
    } else {
      throw err;
    }
  }
}

await moveFile('temp/data.txt', 'archive/data.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 동기 방식&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

try {
  fs.renameSync('old-name.txt', 'new-name.txt');
  console.log('이름 변경 완료');
} catch (err) {
  console.error('오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파일 복사&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 copyFile&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 기본 복사
await fs.copyFile('source.txt', 'dest.txt');

// 대상 파일이 있으면 오류 발생 (덮어쓰기 방지)
await fs.copyFile('source.txt', 'dest.txt', fs.constants.COPYFILE_EXCL);

// 콜백 방식
fs.copyFile('source.txt', 'dest.txt', (err) =&amp;gt; {
  if (err) throw err;
  console.log('복사 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 복사 플래그&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 덮어쓰기 방지
const COPYFILE_EXCL = fs.constants.COPYFILE_EXCL;

// copy-on-write (지원되는 파일 시스템에서)
const COPYFILE_FICLONE = fs.constants.COPYFILE_FICLONE;

// copy-on-write 시도, 실패 시 일반 복사
const COPYFILE_FICLONE_FORCE = fs.constants.COPYFILE_FICLONE_FORCE;

await fs.copyFile('src.txt', 'dst.txt', COPYFILE_EXCL);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 cp - 디렉토리 복사 (Node.js 16.7+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 디렉토리 재귀 복사
await fs.cp('source-folder', 'dest-folder', { recursive: true });

// 옵션
await fs.cp('src', 'dst', {
  recursive: true,        // 하위 디렉토리 포함
  force: true,           // 대상 덮어쓰기
  preserveTimestamps: true,  // 타임스탬프 유지
  filter: (src, dest) =&amp;gt; {   // 필터링
    return !src.includes('node_modules');
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 여러 파일 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 여러 파일 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function deleteFiles(files) {
  const results = await Promise.allSettled(
    files.map(file =&amp;gt; fs.unlink(file))
  );

  results.forEach((result, index) =&amp;gt; {
    if (result.status === 'fulfilled') {
      console.log(`${files[index]} 삭제 완료`);
    } else {
      console.log(`${files[index]} 삭제 실패:`, result.reason.message);
    }
  });
}

await deleteFiles(['temp1.txt', 'temp2.txt', 'temp3.txt']);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 패턴으로 파일 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function deleteByPattern(directory, pattern) {
  const files = await fs.readdir(directory);
  const regex = new RegExp(pattern);

  const deletePromises = files
    .filter(file =&amp;gt; regex.test(file))
    .map(file =&amp;gt; fs.unlink(path.join(directory, file)));

  await Promise.all(deletePromises);
}

// .tmp 파일 모두 삭제
await deleteByPattern('./temp', '\\.tmp$');

// .log 파일 모두 삭제
await deleteByPattern('./logs', '\\.log$');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 여러 파일 이동&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function moveFiles(files, destDir) {
  // 대상 디렉토리 생성
  await fs.mkdir(destDir, { recursive: true });

  const movePromises = files.map(async (file) =&amp;gt; {
    const filename = path.basename(file);
    const dest = path.join(destDir, filename);

    try {
      await fs.rename(file, dest);
      return { file, status: 'success' };
    } catch (err) {
      // 다른 파일 시스템인 경우 복사 후 삭제
      if (err.code === 'EXDEV') {
        await fs.copyFile(file, dest);
        await fs.unlink(file);
        return { file, status: 'success' };
      }
      return { file, status: 'error', error: err };
    }
  });

  return Promise.all(movePromises);
}

const files = ['data1.txt', 'data2.txt', 'data3.txt'];
const results = await moveFiles(files, './archive');
console.log(results);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 안전한 파일 작업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 삭제 전 확인&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function safeDelete(filepath) {
  try {
    const stats = await fs.stat(filepath);

    if (!stats.isFile()) {
      throw new Error('파일이 아닙니다.');
    }

    await fs.unlink(filepath);
    return { success: true };
  } catch (err) {
    if (err.code === 'ENOENT') {
      return { success: true, message: '이미 삭제됨' };
    }
    return { success: false, error: err };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 백업 후 이동&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function moveWithBackup(source, destination) {
  const backupDir = './backup';
  const timestamp = Date.now();
  const filename = path.basename(source);

  // 대상에 파일이 있으면 백업
  try {
    await fs.access(destination);
    const backupPath = path.join(backupDir, `${timestamp}_${filename}`);
    await fs.mkdir(backupDir, { recursive: true });
    await fs.copyFile(destination, backupPath);
    console.log(`기존 파일 백업: ${backupPath}`);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }

  // 파일 이동
  await fs.rename(source, destination);
  console.log(`파일 이동 완료: ${source} &amp;rarr; ${destination}`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 휴지통으로 이동&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

async function moveToTrash(filepath) {
  const trashDir = './.trash';
  const timestamp = Date.now();
  const filename = path.basename(filepath);
  const trashPath = path.join(trashDir, `${timestamp}_${filename}`);

  await fs.mkdir(trashDir, { recursive: true });
  await fs.rename(filepath, trashPath);

  return trashPath;
}

// 휴지통 비우기
async function emptyTrash() {
  const trashDir = './.trash';
  await fs.rm(trashDir, { recursive: true, force: true });
  await fs.mkdir(trashDir, { recursive: true });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실전 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 파일 정리 유틸리티&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

class FileOrganizer {
  constructor(sourceDir, targetDir) {
    this.sourceDir = sourceDir;
    this.targetDir = targetDir;
  }

  async organize() {
    const files = await fs.readdir(this.sourceDir);

    for (const file of files) {
      const sourcePath = path.join(this.sourceDir, file);
      const stats = await fs.stat(sourcePath);

      if (!stats.isFile()) continue;

      const ext = path.extname(file).slice(1).toLowerCase() || 'other';
      const targetFolder = path.join(this.targetDir, ext);

      await fs.mkdir(targetFolder, { recursive: true });
      await fs.rename(sourcePath, path.join(targetFolder, file));

      console.log(`${file} &amp;rarr; ${ext}/`);
    }
  }

  async cleanup(days) {
    const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
    const files = await fs.readdir(this.sourceDir);

    for (const file of files) {
      const filepath = path.join(this.sourceDir, file);
      const stats = await fs.stat(filepath);

      if (stats.mtimeMs &amp;lt; cutoff) {
        await fs.unlink(filepath);
        console.log(`삭제됨: ${file}`);
      }
    }
  }
}

const organizer = new FileOrganizer('./downloads', './organized');
await organizer.organize();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 임시 파일 관리자&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');
const os = require('os');

class TempFileManager {
  constructor() {
    this.tempDir = path.join(os.tmpdir(), 'myapp');
    this.files = new Set();
  }

  async init() {
    await fs.mkdir(this.tempDir, { recursive: true });
  }

  async create(prefix = 'tmp') {
    const filename = `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
    const filepath = path.join(this.tempDir, filename);
    await fs.writeFile(filepath, '');
    this.files.add(filepath);
    return filepath;
  }

  async cleanup() {
    for (const file of this.files) {
      try {
        await fs.unlink(file);
      } catch (err) {
        // 이미 삭제된 경우 무시
      }
    }
    this.files.clear();
  }

  async cleanupAll() {
    await fs.rm(this.tempDir, { recursive: true, force: true });
    await this.init();
    this.files.clear();
  }
}

const temp = new TempFileManager();
await temp.init();
const tempFile = await temp.create('session');
// 작업 수행
await temp.cleanup();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 파일 삭제는 unlink, rm 메서드를 사용하고, 이동은 rename 메서드를 사용합니다. 다른 파일 시스템 간 이동 시에는 copyFile로 복사 후 unlink로 삭제해야 합니다. Node.js 14.14+의 rm과 16.7+의 cp 메서드는 재귀 옵션을 지원하여 디렉토리 작업을 간편하게 합니다. 프로덕션 환경에서는 백업, 휴지통 기능을 구현하여 안전한 파일 관리를 하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/801</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%82%AD%EC%A0%9C-%EB%B0%8F-%EC%9D%B4%EB%8F%99#entry801comment</comments>
      <pubDate>Mon, 2 Mar 2026 12:39:17 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 파일 읽기와 쓰기</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%9D%BD%EA%B8%B0%EC%99%80-%EC%93%B0%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. fs 모듈 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 fs(File System) 모듈은 파일 시스템과 상호작용하기 위한 API를 제공합니다. 동기(Sync), 콜백, Promise 세 가지 방식으로 파일 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 세 가지 API 스타일
const fs = require('fs');           // 콜백 기반
const fsSync = require('fs');       // 동기 (같은 모듈, Sync 접미사 메서드)
const fsPromises = require('fs').promises;  // Promise 기반
// 또는
const { readFile, writeFile } = require('fs/promises');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 파일 읽기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 비동기 읽기 (콜백)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 텍스트 파일 읽기
fs.readFile('example.txt', 'utf8', (err, data) =&amp;gt; {
  if (err) {
    console.error('파일 읽기 오류:', err);
    return;
  }
  console.log(data);
});

// 인코딩 없이 읽기 (Buffer 반환)
fs.readFile('image.png', (err, data) =&amp;gt; {
  if (err) throw err;
  console.log(data);  // &amp;lt;Buffer ...&amp;gt;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 동기 읽기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

try {
  const data = fs.readFileSync('example.txt', 'utf8');
  console.log(data);
} catch (err) {
  console.error('파일 읽기 오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Promise 기반 읽기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function readFileAsync() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('파일 읽기 오류:', err);
  }
}

readFileAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 다양한 읽기 옵션&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 옵션 객체 사용
const data = await fs.readFile('example.txt', {
  encoding: 'utf8',
  flag: 'r'  // 읽기 모드 (기본값)
});

// 주요 flag 값
// 'r'   - 읽기, 파일 없으면 오류
// 'r+'  - 읽기/쓰기, 파일 없으면 오류
// 'w'   - 쓰기, 파일 없으면 생성, 있으면 덮어쓰기
// 'w+'  - 읽기/쓰기, 파일 없으면 생성
// 'a'   - 추가, 파일 없으면 생성
// 'a+'  - 읽기/추가, 파일 없으면 생성&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파일 쓰기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 비동기 쓰기 (콜백)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const content = 'Hello, Node.js!';

fs.writeFile('output.txt', content, 'utf8', (err) =&amp;gt; {
  if (err) {
    console.error('파일 쓰기 오류:', err);
    return;
  }
  console.log('파일이 저장되었습니다.');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 동기 쓰기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

try {
  fs.writeFileSync('output.txt', 'Hello, Node.js!', 'utf8');
  console.log('파일이 저장되었습니다.');
} catch (err) {
  console.error('파일 쓰기 오류:', err);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Promise 기반 쓰기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function writeFileAsync() {
  try {
    await fs.writeFile('output.txt', 'Hello, Node.js!', 'utf8');
    console.log('파일이 저장되었습니다.');
  } catch (err) {
    console.error('파일 쓰기 오류:', err);
  }
}

writeFileAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 다양한 쓰기 옵션&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 옵션 객체 사용
await fs.writeFile('output.txt', 'Hello', {
  encoding: 'utf8',
  mode: 0o644,  // 파일 권한
  flag: 'w'    // 쓰기 모드
});

// JSON 파일 저장
const data = { name: '홍길동', age: 30 };
await fs.writeFile('data.json', JSON.stringify(data, null, 2), 'utf8');&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파일 추가 (Append)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 appendFile 사용&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 파일 끝에 내용 추가
await fs.appendFile('log.txt', '새로운 로그 항목\n', 'utf8');

// 콜백 방식
fs.appendFile('log.txt', '로그 메시지\n', (err) =&amp;gt; {
  if (err) throw err;
  console.log('로그가 추가되었습니다.');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 writeFile의 'a' 플래그 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

await fs.writeFile('log.txt', '새로운 항목\n', { flag: 'a' });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파일 핸들 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 핸들을 사용하면 더 세밀한 제어가 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function useFileHandle() {
  let fileHandle;

  try {
    // 파일 열기
    fileHandle = await fs.open('example.txt', 'r+');

    // 파일 읽기
    const { buffer, bytesRead } = await fileHandle.read(
      Buffer.alloc(100),  // 버퍼
      0,                  // 버퍼 오프셋
      100,                // 읽을 바이트 수
      0                   // 파일 오프셋
    );

    console.log(buffer.toString('utf8', 0, bytesRead));

    // 파일 쓰기
    await fileHandle.write('새로운 내용', 0, 'utf8');

    // 파일 정보 가져오기
    const stats = await fileHandle.stat();
    console.log('파일 크기:', stats.size);

  } finally {
    // 파일 핸들 닫기 (중요!)
    if (fileHandle) {
      await fileHandle.close();
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 부분 읽기/쓰기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 특정 위치에서 읽기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 저수준 API 사용
fs.open('large-file.txt', 'r', (err, fd) =&amp;gt; {
  if (err) throw err;

  const buffer = Buffer.alloc(100);

  // 파일의 50번째 바이트부터 100바이트 읽기
  fs.read(fd, buffer, 0, 100, 50, (err, bytesRead, buffer) =&amp;gt; {
    if (err) throw err;
    console.log(buffer.toString('utf8', 0, bytesRead));
    fs.close(fd, () =&amp;gt; {});
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 특정 위치에 쓰기&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.open('example.txt', 'r+', (err, fd) =&amp;gt; {
  if (err) throw err;

  const data = Buffer.from('INSERT');

  // 파일의 10번째 위치에 쓰기
  fs.write(fd, data, 0, data.length, 10, (err) =&amp;gt; {
    if (err) throw err;
    fs.close(fd, () =&amp;gt; {});
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. JSON 파일 다루기&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// JSON 읽기
async function readJSON(filepath) {
  const data = await fs.readFile(filepath, 'utf8');
  return JSON.parse(data);
}

// JSON 쓰기
async function writeJSON(filepath, data) {
  const json = JSON.stringify(data, null, 2);
  await fs.writeFile(filepath, json, 'utf8');
}

// 사용 예시
async function main() {
  // 읽기
  const config = await readJSON('config.json');
  console.log(config);

  // 수정 후 저장
  config.version = '2.0.0';
  await writeJSON('config.json', config);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 파일 존재 여부 확인&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// access 사용 (권장)
async function fileExists(filepath) {
  try {
    await fs.access(filepath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

// stat 사용
async function checkFile(filepath) {
  try {
    const stats = await fs.stat(filepath);
    return stats.isFile();
  } catch {
    return false;
  }
}

// 동기 방식
const existsSync = fs.existsSync('example.txt');  // true/false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실전 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 설정 파일 관리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;
const path = require('path');

class ConfigManager {
  constructor(configPath) {
    this.configPath = configPath;
  }

  async load() {
    try {
      const data = await fs.readFile(this.configPath, 'utf8');
      return JSON.parse(data);
    } catch (err) {
      if (err.code === 'ENOENT') {
        return {};  // 파일이 없으면 빈 객체 반환
      }
      throw err;
    }
  }

  async save(config) {
    const dir = path.dirname(this.configPath);
    await fs.mkdir(dir, { recursive: true });
    await fs.writeFile(
      this.configPath,
      JSON.stringify(config, null, 2),
      'utf8'
    );
  }

  async update(updates) {
    const config = await this.load();
    const newConfig = { ...config, ...updates };
    await this.save(newConfig);
    return newConfig;
  }
}

// 사용
const config = new ConfigManager('./config/app.json');
await config.update({ theme: 'dark', language: 'ko' });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 로그 파일 관리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

class Logger {
  constructor(logPath) {
    this.logPath = logPath;
  }

  async log(level, message) {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] [${level}] ${message}\n`;

    await fs.appendFile(this.logPath, logEntry, 'utf8');
  }

  info(message) {
    return this.log('INFO', message);
  }

  error(message) {
    return this.log('ERROR', message);
  }

  async readLogs() {
    try {
      return await fs.readFile(this.logPath, 'utf8');
    } catch {
      return '';
    }
  }
}

const logger = new Logger('./logs/app.log');
await logger.info('애플리케이션 시작');
await logger.error('오류 발생');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 fs 모듈은 파일 읽기와 쓰기를 위한 다양한 메서드를 제공합니다. readFile/writeFile로 간단한 작업을, 파일 핸들을 사용해 세밀한 제어를 할 수 있습니다. Promise 기반 API(fs.promises)를 사용하면 async/await과 함께 깔끔한 비동기 코드를 작성할 수 있습니다. 파일 작업 시에는 반드시 에러 처리를 해야 하며, 파일 핸들을 열었다면 반드시 닫아야 합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/800</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%9D%BD%EA%B8%B0%EC%99%80-%EC%93%B0%EA%B8%B0#entry800comment</comments>
      <pubDate>Sat, 28 Feb 2026 14:44:54 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 이벤트 에미터(EventEmitter) 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%97%90%EB%AF%B8%ED%84%B0EventEmitter-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. EventEmitter란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventEmitter는 Node.js에서 이벤트 기반 프로그래밍을 구현하기 위한 핵심 클래스입니다. 이벤트를 발생시키고(emit) 리스너를 등록하여(on) 느슨하게 결합된 비동기 아키텍처를 구축할 수 있습니다. http, fs, stream 등 대부분의 Node.js 핵심 모듈이 EventEmitter를 기반으로 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. EventEmitter 생성&lt;/h2&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;const EventEmitter = require('events');

// 방법 1: 직접 인스턴스 생성
const emitter = new EventEmitter();

// 방법 2: 클래스 상속
class MyEmitter extends EventEmitter {
  constructor() {
    super();
    this.name = 'MyEmitter';
  }
}

const myEmitter = new MyEmitter();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이벤트 등록과 발생&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 on()과 emit()&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const EventEmitter = require('events');
const emitter = new EventEmitter();

// 이벤트 리스너 등록
emitter.on('message', (data) =&amp;gt; {
  console.log('메시지 수신:', data);
});

// 여러 인자 전달
emitter.on('user', (name, age, city) =&amp;gt; {
  console.log(`${name}, ${age}세, ${city}`);
});

// 이벤트 발생
emitter.emit('message', 'Hello World');
// 메시지 수신: Hello World

emitter.emit('user', '홍길동', 30, '서울');
// 홍길동, 30세, 서울&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 addListener() (on의 별칭)&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// on()과 동일
emitter.addListener('event', () =&amp;gt; {
  console.log('이벤트 발생');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 once() - 일회성 리스너&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;emitter.once('connect', () =&amp;gt; {
  console.log('연결됨 (한 번만 실행)');
});

emitter.emit('connect');  // 연결됨 (한 번만 실행)
emitter.emit('connect');  // 아무 일도 없음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 prependListener() - 리스너 맨 앞에 추가&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;emitter.on('data', () =&amp;gt; console.log('리스너 1'));
emitter.on('data', () =&amp;gt; console.log('리스너 2'));
emitter.prependListener('data', () =&amp;gt; console.log('리스너 0'));

emitter.emit('data');
// 리스너 0
// 리스너 1
// 리스너 2&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 리스너 제거&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 off() / removeListener()&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;function handler(data) {
  console.log('데이터:', data);
}

emitter.on('data', handler);
emitter.emit('data', 'test');  // 데이터: test

emitter.off('data', handler);
// 또는 emitter.removeListener('data', handler);

emitter.emit('data', 'test');  // 아무 출력 없음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 removeAllListeners()&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;emitter.on('event', () =&amp;gt; console.log('1'));
emitter.on('event', () =&amp;gt; console.log('2'));
emitter.on('other', () =&amp;gt; console.log('other'));

// 특정 이벤트의 모든 리스너 제거
emitter.removeAllListeners('event');

// 모든 이벤트의 모든 리스너 제거
emitter.removeAllListeners();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 이벤트 정보 조회&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

emitter.on('data', () =&amp;gt; {});
emitter.on('data', () =&amp;gt; {});
emitter.on('error', () =&amp;gt; {});

// 이벤트 이름 목록
console.log(emitter.eventNames());
// ['data', 'error']

// 특정 이벤트의 리스너 수
console.log(emitter.listenerCount('data'));
// 2

// 특정 이벤트의 리스너 배열
console.log(emitter.listeners('data'));
// [Function, Function]

// 원본 리스너 (once로 등록해도 래핑되지 않은 원본)
console.log(emitter.rawListeners('data'));&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 에러 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;error 이벤트는 특별하게 처리됩니다. 리스너가 없으면 예외가 발생하고 프로세스가 종료됩니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// error 리스너 없이 emit하면 예외 발생
// emitter.emit('error', new Error('문제 발생'));
// &amp;rarr; 프로세스 종료

// error 리스너 등록
emitter.on('error', (err) =&amp;gt; {
  console.error('에러 처리:', err.message);
});

emitter.emit('error', new Error('문제 발생'));
// 에러 처리: 문제 발생 (프로세스 계속 실행)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 리스너 수 제한&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 이벤트당 10개의 리스너 제한이 있습니다. 메모리 누수 방지를 위한 설정입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// 현재 최대 리스너 수 확인
console.log(emitter.getMaxListeners());  // 10

// 최대 리스너 수 변경
emitter.setMaxListeners(20);

// 전역 기본값 변경
EventEmitter.defaultMaxListeners = 15;

// 무제한 (권장하지 않음)
emitter.setMaxListeners(0);
// 또는
emitter.setMaxListeners(Infinity);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 비동기 이벤트 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 async 리스너&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

emitter.on('fetch', async (url) =&amp;gt; {
  try {
    const response = await fetch(url);
    const data = await response.json();
    emitter.emit('success', data);
  } catch (err) {
    emitter.emit('error', err);
  }
});

emitter.on('success', (data) =&amp;gt; {
  console.log('성공:', data);
});

emitter.on('error', (err) =&amp;gt; {
  console.error('실패:', err.message);
});

emitter.emit('fetch', 'https://api.example.com/data');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 events.on() - 비동기 이터레이터&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const { on } = require('events');

async function processEvents(emitter) {
  for await (const [data] of on(emitter, 'data')) {
    console.log('받은 데이터:', data);
    if (data === 'end') break;
  }
}

const emitter = new EventEmitter();
processEvents(emitter);

emitter.emit('data', '첫 번째');
emitter.emit('data', '두 번째');
emitter.emit('data', 'end');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 events.once() - Promise 반환&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { once } = require('events');

async function waitForConnect(emitter) {
  const [connection] = await once(emitter, 'connect');
  console.log('연결됨:', connection);
}

const emitter = new EventEmitter();
waitForConnect(emitter);

setTimeout(() =&amp;gt; {
  emitter.emit('connect', { host: 'localhost', port: 3000 });
}, 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실전 예시: 커스텀 클래스&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const EventEmitter = require('events');

class TaskQueue extends EventEmitter {
  constructor() {
    super();
    this.tasks = [];
    this.running = false;
  }

  add(task) {
    this.tasks.push(task);
    this.emit('taskAdded', task);

    if (!this.running) {
      this.process();
    }
  }

  async process() {
    this.running = true;
    this.emit('start');

    while (this.tasks.length &amp;gt; 0) {
      const task = this.tasks.shift();
      this.emit('taskStart', task);

      try {
        const result = await task();
        this.emit('taskComplete', { task, result });
      } catch (err) {
        this.emit('taskError', { task, error: err });
      }
    }

    this.running = false;
    this.emit('empty');
  }
}

// 사용
const queue = new TaskQueue();

queue.on('taskAdded', (task) =&amp;gt; console.log('작업 추가됨'));
queue.on('taskStart', (task) =&amp;gt; console.log('작업 시작'));
queue.on('taskComplete', ({ result }) =&amp;gt; console.log('완료:', result));
queue.on('taskError', ({ error }) =&amp;gt; console.error('에러:', error.message));
queue.on('empty', () =&amp;gt; console.log('모든 작업 완료'));

queue.add(async () =&amp;gt; {
  await new Promise(r =&amp;gt; setTimeout(r, 100));
  return '작업 1 결과';
});

queue.add(async () =&amp;gt; {
  await new Promise(r =&amp;gt; setTimeout(r, 100));
  return '작업 2 결과';
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. captureRejections 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async 리스너의 에러를 자동으로 error 이벤트로 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const emitter = new EventEmitter({ captureRejections: true });

emitter.on('event', async () =&amp;gt; {
  throw new Error('비동기 에러');
});

emitter.on('error', (err) =&amp;gt; {
  console.error('캡처된 에러:', err.message);
});

emitter.emit('event');
// 캡처된 에러: 비동기 에러&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EventEmitter는 Node.js 이벤트 기반 아키텍처의 핵심입니다. on/emit으로 이벤트를 등록하고 발생시키며, once로 일회성 리스너를 등록합니다. error 이벤트는 반드시 처리해야 하고, 클래스 상속을 통해 커스텀 이벤트 기반 객체를 만들 수 있습니다. events.once()와 events.on()을 활용하면 Promise 기반으로 더 깔끔하게 이벤트를 처리할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/799</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%97%90%EB%AF%B8%ED%84%B0EventEmitter-%EC%82%AC%EC%9A%A9%EB%B2%95#entry799comment</comments>
      <pubDate>Wed, 25 Feb 2026 20:00:32 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 타이머 함수(Timer Functions)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%83%80%EC%9D%B4%EB%A8%B8-%ED%95%A8%EC%88%98Timer-Functions</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 타이머 함수란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이머 함수는 일정 시간 후 또는 주기적으로 코드를 실행하기 위한 글로벌 함수입니다. Node.js의 타이머는 브라우저의 타이머 API와 유사하지만, 이벤트 루프와 밀접하게 연관되어 있습니다. setTimeout, setInterval, setImmediate, process.nextTick이 주요 타이머 함수입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. setTimeout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정된 시간 후 콜백 함수를 한 번 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 2초 후 실행
setTimeout(() =&amp;gt; {
  console.log('2초 후 실행됨');
}, 2000);

// 인자 전달
setTimeout((name, age) =&amp;gt; {
  console.log(`이름: ${name}, 나이: ${age}`);
}, 1000, '홍길동', 30);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 타이머 취소&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const timerId = setTimeout(() =&amp;gt; {
  console.log('이 메시지는 출력되지 않습니다');
}, 5000);

// 타이머 취소
clearTimeout(timerId);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Promise 기반 타이머 (Node.js 15+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { setTimeout } = require('timers/promises');

async function delay() {
  console.log('시작');
  await setTimeout(2000);
  console.log('2초 후');

  // 값 반환
  const result = await setTimeout(1000, '완료');
  console.log(result);  // 완료
}

delay();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. setInterval&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정된 간격으로 콜백 함수를 반복 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;let count = 0;

const intervalId = setInterval(() =&amp;gt; {
  count++;
  console.log(`실행 횟수: ${count}`);

  // 5번 후 중지
  if (count &amp;gt;= 5) {
    clearInterval(intervalId);
    console.log('반복 종료');
  }
}, 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 타이머 취소&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const intervalId = setInterval(() =&amp;gt; {
  console.log('매 초마다 실행');
}, 1000);

// 5초 후 인터벌 취소
setTimeout(() =&amp;gt; {
  clearInterval(intervalId);
  console.log('인터벌 취소됨');
}, 5000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Promise 기반 반복 타이머 (Node.js 15+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { setInterval } = require('timers/promises');

async function periodicTask() {
  let count = 0;

  for await (const _ of setInterval(1000)) {
    count++;
    console.log(`반복 ${count}`);

    if (count &amp;gt;= 5) break;
  }
}

periodicTask();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. setImmediate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 이벤트 루프 사이클이 완료된 후 콜백을 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;console.log('1: 시작');

setImmediate(() =&amp;gt; {
  console.log('3: setImmediate');
});

console.log('2: 끝');

// 출력 순서: 1 &amp;rarr; 2 &amp;rarr; 3&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 setTimeout(0)과의 차이&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;setImmediate(() =&amp;gt; {
  console.log('setImmediate');
});

setTimeout(() =&amp;gt; {
  console.log('setTimeout');
}, 0);

// 실행 순서는 상황에 따라 다를 수 있음
// I/O 콜백 내부에서는 setImmediate가 항상 먼저 실행됨&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 I/O 콜백 내부에서의 실행 순서&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.readFile('file.txt', () =&amp;gt; {
  setTimeout(() =&amp;gt; {
    console.log('setTimeout');
  }, 0);

  setImmediate(() =&amp;gt; {
    console.log('setImmediate');
  });
});

// I/O 콜백 내부에서는 항상:
// setImmediate &amp;rarr; setTimeout 순서&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 Promise 기반 (Node.js 15+)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { setImmediate } = require('timers/promises');

async function immediate() {
  await setImmediate();
  console.log('즉시 실행 (다음 틱)');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. process.nextTick&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작업이 완료된 직후, 이벤트 루프 다음 단계 전에 콜백을 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;console.log('1: 시작');

process.nextTick(() =&amp;gt; {
  console.log('2: nextTick');
});

console.log('3: 끝');

// 출력 순서: 1 &amp;rarr; 3 &amp;rarr; 2&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 실행 순서 비교&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;console.log('1: 동기');

setTimeout(() =&amp;gt; console.log('5: setTimeout'), 0);
setImmediate(() =&amp;gt; console.log('6: setImmediate'));

Promise.resolve().then(() =&amp;gt; console.log('3: Promise'));
process.nextTick(() =&amp;gt; console.log('2: nextTick'));

console.log('4: 동기');

// 출력 순서: 1 &amp;rarr; 4 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 5/6 (5와 6은 상황에 따라 다름)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 재귀 호출 주의&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 잘못된 사용 - 이벤트 루프 블로킹
function badRecursion() {
  process.nextTick(badRecursion);  // 무한 루프!
}

// 올바른 사용 - setImmediate 사용
function goodRecursion() {
  setImmediate(goodRecursion);  // 다른 작업 처리 가능
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 타이머 객체 메서드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 ref(), unref()&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const timer = setTimeout(() =&amp;gt; {
  console.log('실행됨');
}, 10000);

// unref: 이 타이머가 프로세스를 유지하지 않음
timer.unref();

// 다른 작업이 없으면 프로세스가 종료됨
console.log('프로세스가 바로 종료될 수 있음');

// ref: 다시 프로세스를 유지하도록 설정
timer.ref();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 refresh()&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const timer = setTimeout(() =&amp;gt; {
  console.log('실행됨');
}, 5000);

// 2초 후 타이머 재시작 (다시 5초 대기)
setTimeout(() =&amp;gt; {
  timer.refresh();
  console.log('타이머 재시작');
}, 2000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 디바운스 (Debounce)&lt;/h3&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function debounce(func, delay) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() =&amp;gt; {
      func.apply(this, args);
    }, delay);
  };
}

const search = debounce((query) =&amp;gt; {
  console.log('검색:', query);
}, 300);

search('h');
search('he');
search('hel');
search('hell');
search('hello');  // 마지막 호출만 실행됨&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 쓰로틀 (Throttle)&lt;/h3&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function throttle(func, limit) {
  let inThrottle;

  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() =&amp;gt; {
        inThrottle = false;
      }, limit);
    }
  };
}

const handleScroll = throttle(() =&amp;gt; {
  console.log('스크롤 처리');
}, 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 재시도 로직&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchWithRetry(url, maxRetries = 3, delay = 1000) {
  for (let i = 0; i &amp;lt; maxRetries; i++) {
    try {
      const response = await fetch(url);
      return await response.json();
    } catch (err) {
      if (i === maxRetries - 1) throw err;

      console.log(`재시도 ${i + 1}/${maxRetries}...`);
      await new Promise(resolve =&amp;gt; setTimeout(resolve, delay * (i + 1)));
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 타이머 함수는 비동기 작업 스케줄링의 핵심 도구입니다. setTimeout은 지연 실행, setInterval은 반복 실행, setImmediate는 현재 이벤트 루프 완료 후 실행, process.nextTick은 현재 작업 직후 실행에 사용됩니다. Node.js 15+에서는 timers/promises를 통해 Promise 기반으로 더 깔끔하게 사용할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/798</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%83%80%EC%9D%B4%EB%A8%B8-%ED%95%A8%EC%88%98Timer-Functions#entry798comment</comments>
      <pubDate>Wed, 25 Feb 2026 14:00:16 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 콘솔 객체(console object)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BD%98%EC%86%94-%EA%B0%9D%EC%B2%B4console-object</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. console 객체란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console은 Node.js에서 표준 출력(stdout)과 표준 에러(stderr)로 메시지를 출력하기 위한 글로벌 객체입니다. 디버깅, 로깅, 성능 측정 등에 사용되며, 브라우저의 console과 유사한 API를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 출력 메서드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 log, info, warn, error&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 표준 출력 (stdout)
console.log('일반 로그 메시지');
console.info('정보 메시지');

// 표준 에러 (stderr)
console.warn('경고 메시지');
console.error('에러 메시지');

// 출력 스트림 차이
// console.log &amp;rarr; process.stdout
// console.error &amp;rarr; process.stderr&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 형식 지정자&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;// %s - 문자열
console.log('이름: %s', '홍길동');
// 이름: 홍길동

// %d, %i - 정수
console.log('나이: %d세', 30);
// 나이: 30세

// %f - 부동소수점
console.log('가격: %f원', 1234.56);
// 가격: 1234.56원

// %j - JSON
console.log('객체: %j', { name: 'test', value: 123 });
// 객체: {&quot;name&quot;:&quot;test&quot;,&quot;value&quot;:123}

// %o, %O - 객체
console.log('객체: %o', { name: 'test' });
// 객체: { name: 'test' }

// %% - 퍼센트 기호
console.log('진행률: %d%%', 75);
// 진행률: 75%&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 객체 출력&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 console.dir()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 상세하게 출력합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const obj = {
  name: 'test',
  nested: {
    level1: {
      level2: {
        level3: 'deep value'
      }
    }
  }
};

// 기본 출력 (깊이 2까지)
console.dir(obj);

// 옵션 지정
console.dir(obj, {
  depth: null,     // 모든 깊이 표시 (기본값: 2)
  colors: true,    // 색상 출력
  showHidden: true // 숨겨진 속성 표시
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 console.table()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열이나 객체를 테이블 형식으로 출력합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 배열 테이블
const users = [
  { name: '홍길동', age: 30, city: '서울' },
  { name: '김철수', age: 25, city: '부산' },
  { name: '이영희', age: 28, city: '대구' }
];

console.table(users);
/*
┌─────────┬──────────┬─────┬────────┐
│ (index) │   name   │ age │  city  │
├─────────┼──────────┼─────┼────────┤
│    0    │ '홍길동' │ 30  │ '서울' │
│    1    │ '김철수' │ 25  │ '부산' │
│    2    │ '이영희' │ 28  │ '대구' │
└─────────┴──────────┴─────┴────────┘
*/

// 특정 컬럼만 출력
console.table(users, ['name', 'age']);

// 객체 테이블
const scores = {
  math: 90,
  english: 85,
  science: 95
};

console.table(scores);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 시간 측정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 time, timeEnd, timeLog&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 시간 측정 시작
console.time('작업');

// 작업 수행
for (let i = 0; i &amp;lt; 1000000; i++) {
  // 반복 작업
}

// 중간 시간 출력 (타이머 계속 유지)
console.timeLog('작업', '중간 체크');
// 작업: 50.123ms 중간 체크

// 시간 측정 종료
console.timeEnd('작업');
// 작업: 123.456ms&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 여러 타이머 동시 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;console.time('전체');
console.time('작업1');

// 작업 1 수행
await task1();
console.timeEnd('작업1');  // 작업1: 100ms

console.time('작업2');
// 작업 2 수행
await task2();
console.timeEnd('작업2');  // 작업2: 200ms

console.timeEnd('전체');   // 전체: 300ms&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 카운터&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 호출 횟수 카운트
console.count('myCounter');  // myCounter: 1
console.count('myCounter');  // myCounter: 2
console.count('myCounter');  // myCounter: 3

// 다른 레이블
console.count('other');      // other: 1
console.count('myCounter');  // myCounter: 4

// 카운터 리셋
console.countReset('myCounter');
console.count('myCounter');  // myCounter: 1&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그룹화&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;console.group('사용자 정보');
console.log('이름: 홍길동');
console.log('나이: 30');

console.group('주소');
console.log('도시: 서울');
console.log('구: 강남구');
console.groupEnd();

console.groupEnd();

/*
출력:
사용자 정보
  이름: 홍길동
  나이: 30
  주소
    도시: 서울
    구: 강남구
*/

// 접힌 그룹 (브라우저에서 더 유용)
console.groupCollapsed('상세 정보');
console.log('숨겨진 내용');
console.groupEnd();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 조건부 출력과 스택 트레이스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 console.assert()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 false일 때만 출력합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const value = 10;

console.assert(value &amp;gt; 5, 'value는 5보다 커야 합니다');
// 조건이 true이므로 출력 없음

console.assert(value &amp;gt; 15, 'value는 15보다 커야 합니다');
// Assertion failed: value는 15보다 커야 합니다&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 console.trace()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 스택을 출력합니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;function outer() {
  inner();
}

function inner() {
  console.trace('호출 스택 추적');
}

outer();
/*
Trace: 호출 스택 추적
    at inner (/path/to/file.js:6:11)
    at outer (/path/to/file.js:2:3)
    at Object.&amp;lt;anonymous&amp;gt; (/path/to/file.js:9:1)
*/&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 출력 지우기&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 콘솔 화면 지우기 (터미널에서 동작)
console.clear();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 커스텀 Console 인스턴스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일이나 다른 스트림으로 출력할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');
const { Console } = require('console');

// 파일로 로그 출력
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');

const logger = new Console({
  stdout: output,
  stderr: errorOutput
});

logger.log('이 메시지는 stdout.log에 저장됩니다');
logger.error('이 메시지는 stderr.log에 저장됩니다');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 색상 출력&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ANSI 이스케이프 코드 사용
console.log('\x1b[31m빨간색 텍스트\x1b[0m');
console.log('\x1b[32m초록색 텍스트\x1b[0m');
console.log('\x1b[33m노란색 텍스트\x1b[0m');
console.log('\x1b[34m파란색 텍스트\x1b[0m');

// 배경색
console.log('\x1b[41m빨간 배경\x1b[0m');

// 스타일
console.log('\x1b[1m굵은 텍스트\x1b[0m');
console.log('\x1b[4m밑줄 텍스트\x1b[0m');

// util.inspect.colors 사용
const util = require('util');
console.log(util.inspect({ name: 'test' }, { colors: true }));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console 객체는 Node.js에서 디버깅과 로깅을 위한 핵심 도구입니다. log, error, warn으로 기본 출력을 하고, time/timeEnd로 성능을 측정하며, table로 데이터를 보기 좋게 출력할 수 있습니다. 프로덕션 환경에서는 winston이나 pino 같은 전문 로깅 라이브러리를 사용하는 것이 권장됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/797</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BD%98%EC%86%94-%EA%B0%9D%EC%B2%B4console-object#entry797comment</comments>
      <pubDate>Wed, 25 Feb 2026 11:00:03 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 프로세스 객체(process object)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B0%9D%EC%B2%B4process-object</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. process 객체란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process는 현재 실행 중인 Node.js 프로세스에 대한 정보와 제어 기능을 제공하는 글로벌 객체입니다. 환경 변수 접근, 명령줄 인자 파싱, 프로세스 종료, 표준 입출력 스트림 등 시스템 수준의 다양한 기능을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로세스 정보&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 정보&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;// Node.js 버전 정보
console.log(process.version);      // v20.10.0
console.log(process.versions);     // { node: '20.10.0', v8: '11.3.244.8', ... }

// 프로세스 ID
console.log(process.pid);          // 12345
console.log(process.ppid);         // 부모 프로세스 ID

// 플랫폼 정보
console.log(process.platform);     // darwin, win32, linux
console.log(process.arch);         // x64, arm64

// 실행 경로
console.log(process.execPath);     // /usr/local/bin/node
console.log(process.cwd());        // 현재 작업 디렉토리

// 실행 시간 (초)
console.log(process.uptime());     // 프로세스 실행 시간&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 메모리 사용량&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const memoryUsage = process.memoryUsage();
console.log(memoryUsage);
/*
{
  rss: 30000000,        // 전체 메모리 사용량
  heapTotal: 6000000,   // V8 힙 전체 크기
  heapUsed: 4000000,    // V8 힙 사용량
  external: 1000000,    // V8 외부 메모리 (Buffer 등)
  arrayBuffers: 500000  // ArrayBuffer 메모리
}
*/

// 사람이 읽기 쉬운 형식으로 변환
function formatMemory(bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}

console.log('RSS:', formatMemory(memoryUsage.rss));
console.log('Heap Used:', formatMemory(memoryUsage.heapUsed));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 CPU 사용량&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// CPU 사용 시간 (마이크로초)
const cpuUsage = process.cpuUsage();
console.log(cpuUsage);
// { user: 100000, system: 50000 }

// 이전 측정값과 비교
const startUsage = process.cpuUsage();
// ... 작업 수행
const endUsage = process.cpuUsage(startUsage);
console.log('사용자 CPU:', endUsage.user, '&amp;mu;s');
console.log('시스템 CPU:', endUsage.system, '&amp;mu;s');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 환경 변수&lt;/h2&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;// 환경 변수 읽기
console.log(process.env.NODE_ENV);   // development
console.log(process.env.PATH);
console.log(process.env.HOME);

// 환경 변수 설정
process.env.MY_VAR = 'value';
console.log(process.env.MY_VAR);     // value

// 환경 변수 삭제
delete process.env.MY_VAR;

// 모든 환경 변수 출력
console.log(process.env);

// 환경 변수 기반 설정
const config = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  dbUrl: process.env.DATABASE_URL
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 명령줄 인자&lt;/h2&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;// node app.js arg1 arg2 --name=value
console.log(process.argv);
/*
[
  '/usr/local/bin/node',  // Node.js 실행 경로
  '/path/to/app.js',      // 스크립트 경로
  'arg1',                 // 첫 번째 인자
  'arg2',                 // 두 번째 인자
  '--name=value'          // 옵션 인자
]
*/

// 실제 인자만 추출
const args = process.argv.slice(2);
console.log(args);  // ['arg1', 'arg2', '--name=value']

// 간단한 인자 파싱
function parseArgs(args) {
  const result = { _: [] };

  for (const arg of args) {
    if (arg.startsWith('--')) {
      const [key, value] = arg.slice(2).split('=');
      result[key] = value || true;
    } else if (arg.startsWith('-')) {
      result[arg.slice(1)] = true;
    } else {
      result._.push(arg);
    }
  }

  return result;
}

console.log(parseArgs(process.argv.slice(2)));
// { _: ['arg1', 'arg2'], name: 'value' }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프로세스 종료&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 정상 종료&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// 종료 코드와 함께 종료
process.exit(0);  // 성공
process.exit(1);  // 에러

// 종료 코드 설정 (즉시 종료하지 않음)
process.exitCode = 1;

// 비동기 작업 완료 후 종료
async function cleanup() {
  await saveData();
  process.exit(0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 종료 이벤트&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// 프로세스 종료 직전 (동기 코드만 실행 가능)
process.on('exit', (code) =&amp;gt; {
  console.log('프로세스 종료, 코드:', code);
  // 비동기 작업 불가
});

// 이벤트 루프가 비어있을 때 (비동기 작업 가능)
process.on('beforeExit', (code) =&amp;gt; {
  console.log('beforeExit, 코드:', code);
  // 여기서 비동기 작업을 추가하면 프로세스가 계속 실행됨
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 시그널 처리&lt;/h2&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// SIGINT (Ctrl+C)
process.on('SIGINT', () =&amp;gt; {
  console.log('Ctrl+C 감지');
  process.exit(0);
});

// SIGTERM (종료 시그널)
process.on('SIGTERM', () =&amp;gt; {
  console.log('SIGTERM 수신, 정리 작업 수행...');
  cleanup().then(() =&amp;gt; process.exit(0));
});

// SIGHUP (터미널 종료)
process.on('SIGHUP', () =&amp;gt; {
  console.log('터미널 연결 종료');
});

// Windows에서는 일부 시그널이 지원되지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 표준 입출력 스트림&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 stdout, stderr&lt;/h3&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;// 표준 출력
process.stdout.write('Hello\n');

// 표준 에러
process.stderr.write('에러 메시지\n');

// TTY 확인
if (process.stdout.isTTY) {
  console.log('터미널에서 실행 중');
  console.log('터미널 크기:', process.stdout.columns, 'x', process.stdout.rows);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 stdin&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 표준 입력 읽기
process.stdin.setEncoding('utf8');

process.stdin.on('data', (data) =&amp;gt; {
  console.log('입력:', data.trim());
});

// 한 줄씩 읽기
const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.question('이름을 입력하세요: ', (answer) =&amp;gt; {
  console.log('안녕하세요,', answer);
  rl.close();
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// 처리되지 않은 예외
process.on('uncaughtException', (err) =&amp;gt; {
  console.error('처리되지 않은 예외:', err);
  // 로그 저장 후 종료 권장
  process.exit(1);
});

// 처리되지 않은 프로미스 거부
process.on('unhandledRejection', (reason, promise) =&amp;gt; {
  console.error('처리되지 않은 프로미스 거부:', reason);
});

// 경고 메시지
process.on('warning', (warning) =&amp;gt; {
  console.warn('경고:', warning.name, warning.message);
});

// 경고 발생시키기
process.emitWarning('이것은 경고입니다', 'CustomWarning');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 기타 유용한 메서드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 고해상도 시간 측정
const start = process.hrtime.bigint();
// ... 작업 수행
const end = process.hrtime.bigint();
console.log(`소요 시간: ${end - start} 나노초`);

// 다음 틱에 콜백 실행
process.nextTick(() =&amp;gt; {
  console.log('다음 틱에서 실행');
});

// 작업 디렉토리 변경
process.chdir('/tmp');
console.log(process.cwd());  // /tmp

// 사용자/그룹 ID (Unix)
console.log(process.getuid());  // 사용자 ID
console.log(process.getgid());  // 그룹 ID&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process 객체는 Node.js 프로세스의 정보 조회와 제어를 위한 핵심 글로벌 객체입니다. 환경 변수(process.env), 명령줄 인자(process.argv), 종료 처리(process.exit), 시그널 처리, 표준 입출력 스트림 등 시스템 수준의 다양한 기능을 제공합니다. 프로덕션 환경에서는 uncaughtException과 unhandledRejection 이벤트를 반드시 처리해야 합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/796</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B0%9D%EC%B2%B4process-object#entry796comment</comments>
      <pubDate>Wed, 25 Feb 2026 08:00:48 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 글로벌 객체(Global Objects)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EA%B8%80%EB%A1%9C%EB%B2%8C-%EA%B0%9D%EC%B2%B4Global-Objects</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 글로벌 객체란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 객체는 Node.js 어디서든 별도의 require 없이 사용할 수 있는 객체들입니다. 브라우저의 window 객체와 유사하게, Node.js는 global 객체를 최상위 스코프로 제공합니다. 하지만 모듈 시스템 특성상 var로 선언한 변수는 global에 추가되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. global 객체&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// global 객체 확인
console.log(global);

// global에 속성 추가 (권장하지 않음)
global.myVar = 'Hello';
console.log(myVar);  // Hello (어디서든 접근 가능)

// 브라우저의 window와 비교
// 브라우저: window.document, window.location
// Node.js: global.process, global.Buffer&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 글로벌 객체/함수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 __dirname, __filename&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 모듈의 경로 정보입니다. (CommonJS에서만 사용 가능)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 현재 파일의 절대 경로
console.log(__filename);
// /home/user/project/app.js

// 현재 파일이 있는 디렉토리 경로
console.log(__dirname);
// /home/user/project

// ES Modules에서는 다르게 처리
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 console&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 출력과 에러 출력을 위한 객체입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;console.log('일반 로그');
console.error('에러 로그');
console.warn('경고 로그');
console.info('정보 로그');

// 객체 출력
console.dir({ name: 'test', nested: { value: 123 } }, { depth: null });

// 테이블 형식 출력
console.table([
  { name: '홍길동', age: 30 },
  { name: '김철수', age: 25 }
]);

// 시간 측정
console.time('작업');
// ... 작업 수행
console.timeEnd('작업');  // 작업: 123.456ms

// 조건부 로그
console.assert(1 === 2, '1은 2가 아닙니다');

// 호출 스택 출력
console.trace('여기서 호출됨');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 process&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Node.js 프로세스에 대한 정보와 제어를 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 프로세스 정보
console.log(process.pid);        // 프로세스 ID
console.log(process.version);    // Node.js 버전
console.log(process.platform);   // 운영체제 (darwin, win32, linux)
console.log(process.arch);       // CPU 아키텍처 (x64, arm64)
console.log(process.cwd());      // 현재 작업 디렉토리
console.log(process.memoryUsage()); // 메모리 사용량

// 환경 변수
console.log(process.env.NODE_ENV);
console.log(process.env.PATH);

// 명령줄 인자
console.log(process.argv);
// ['node', '/path/to/script.js', 'arg1', 'arg2']&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Buffer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 데이터를 다루는 클래스입니다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;const buf = Buffer.from('Hello');
console.log(buf);  // &amp;lt;Buffer 48 65 6c 6c 6f&amp;gt;
console.log(buf.toString());  // Hello&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5 타이머 함수&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 지정 시간 후 실행
const timeoutId = setTimeout(() =&amp;gt; {
  console.log('3초 후 실행');
}, 3000);

// 반복 실행
const intervalId = setInterval(() =&amp;gt; {
  console.log('1초마다 실행');
}, 1000);

// 현재 이벤트 루프 후 실행
setImmediate(() =&amp;gt; {
  console.log('즉시 실행 (이벤트 루프 다음)');
});

// 타이머 취소
clearTimeout(timeoutId);
clearInterval(intervalId);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 모듈 관련 글로벌&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 require, module, exports (CommonJS)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 모듈 불러오기
const fs = require('fs');
const myModule = require('./myModule');

// 모듈 내보내기
module.exports = { name: 'test' };
exports.func = () =&amp;gt; {};

// 모듈 정보
console.log(module.id);        // 모듈 식별자
console.log(module.filename);  // 파일 경로
console.log(module.loaded);    // 로드 완료 여부
console.log(module.paths);     // 모듈 검색 경로&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 require.resolve, require.cache&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 모듈 경로 확인 (로드하지 않음)
const modulePath = require.resolve('express');
console.log(modulePath);

// 캐시된 모듈 확인
console.log(require.cache);

// 캐시 삭제 (재로드용)
delete require.cache[require.resolve('./myModule')];&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. URL, URLSearchParams&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHATWG URL API가 글로벌로 제공됩니다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const myUrl = new URL('https://example.com/path?name=test');
console.log(myUrl.hostname);  // example.com
console.log(myUrl.searchParams.get('name'));  // test

const params = new URLSearchParams('a=1&amp;amp;b=2');
console.log(params.get('a'));  // 1&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. TextEncoder, TextDecoder&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열과 바이너리 데이터 변환을 위한 API입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 문자열 &amp;rarr; Uint8Array
const encoder = new TextEncoder();
const encoded = encoder.encode('Hello');
console.log(encoded);  // Uint8Array(5) [72, 101, 108, 108, 111]

// Uint8Array &amp;rarr; 문자열
const decoder = new TextDecoder();
const decoded = decoder.decode(encoded);
console.log(decoded);  // Hello&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 기타 글로벌&lt;/h2&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// 이벤트 처리
const eventEmitter = new EventEmitter();  // X - require 필요

// AbortController (Node.js 15+)
const controller = new AbortController();
const signal = controller.signal;

signal.addEventListener('abort', () =&amp;gt; {
  console.log('작업 취소됨');
});

controller.abort();

// queueMicrotask
queueMicrotask(() =&amp;gt; {
  console.log('마이크로태스크');
});

// structuredClone (Node.js 17+)
const obj = { a: 1, b: { c: 2 } };
const clone = structuredClone(obj);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. globalThis&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES2020에서 도입된 환경 독립적인 전역 객체 참조입니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;// 브라우저: globalThis === window
// Node.js: globalThis === global
// Worker: globalThis === self

console.log(globalThis === global);  // true (Node.js)

// 환경에 관계없이 동작하는 코드
globalThis.myGlobal = 'value';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 글로벌 객체는 require 없이 어디서든 사용할 수 있는 내장 객체들입니다. global, process, console, Buffer, 타이머 함수(__dirname, __filename 등)가 대표적입니다. global 객체에 변수를 추가하는 것은 권장되지 않으며, 모듈 시스템을 통한 명시적인 의존성 관리가 좋은 방식입니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/795</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EA%B8%80%EB%A1%9C%EB%B2%8C-%EA%B0%9D%EC%B2%B4Global-Objects#entry795comment</comments>
      <pubDate>Tue, 24 Feb 2026 20:00:33 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 버퍼(Buffer) 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%B2%84%ED%8D%BCBuffer-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Buffer란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Buffer는 Node.js에서 바이너리 데이터를 다루기 위한 클래스입니다. JavaScript는 원래 문자열 처리에 최적화되어 있어 바이너리 데이터를 직접 다루기 어려웠습니다. Buffer는 고정 크기의 메모리 청크를 할당하여 TCP 스트림, 파일 시스템 작업, 이미지 처리 등에서 바이너리 데이터를 효율적으로 처리할 수 있게 해줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Buffer 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Buffer.alloc()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정된 크기의 버퍼를 생성하고 0으로 초기화합니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 10바이트 버퍼 생성 (0으로 초기화)
const buf1 = Buffer.alloc(10);
console.log(buf1);
// &amp;lt;Buffer 00 00 00 00 00 00 00 00 00 00&amp;gt;

// 특정 값으로 채우기
const buf2 = Buffer.alloc(10, 1);
console.log(buf2);
// &amp;lt;Buffer 01 01 01 01 01 01 01 01 01 01&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Buffer.allocUnsafe()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 없이 버퍼를 생성합니다. 빠르지만 이전 메모리 데이터가 남아있을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 초기화 없이 생성 (더 빠름, 민감한 데이터 주의)
const buf = Buffer.allocUnsafe(10);
console.log(buf);  // 이전 메모리 내용이 있을 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Buffer.from()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터로부터 버퍼를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 문자열로부터 생성
const buf1 = Buffer.from('Hello');
console.log(buf1);
// &amp;lt;Buffer 48 65 6c 6c 6f&amp;gt;

// 인코딩 지정
const buf2 = Buffer.from('안녕하세요', 'utf8');
console.log(buf2);

// 배열로부터 생성
const buf3 = Buffer.from([72, 101, 108, 108, 111]);
console.log(buf3.toString());  // Hello

// 다른 버퍼로부터 복사
const buf4 = Buffer.from(buf1);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Buffer 읽기/쓰기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 문자열 변환&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const buf = Buffer.from('Hello, Node.js!');

// 버퍼를 문자열로 변환
console.log(buf.toString());          // Hello, Node.js!
console.log(buf.toString('utf8'));    // Hello, Node.js!
console.log(buf.toString('hex'));     // 48656c6c6f2c204e6f64652e6a7321
console.log(buf.toString('base64'));  // SGVsbG8sIE5vZGUuanMh

// 부분 문자열
console.log(buf.toString('utf8', 0, 5));  // Hello&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 개별 바이트 접근&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const buf = Buffer.from('Hello');

// 읽기
console.log(buf[0]);  // 72 (H의 ASCII 코드)
console.log(buf[1]);  // 101 (e의 ASCII 코드)

// 쓰기
buf[0] = 74;  // J로 변경
console.log(buf.toString());  // Jello&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 숫자 읽기/쓰기&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const buf = Buffer.alloc(8);

// 정수 쓰기
buf.writeUInt8(255, 0);           // 1바이트 부호 없는 정수
buf.writeUInt16BE(65535, 1);      // 2바이트 빅엔디안
buf.writeUInt32LE(4294967295, 3); // 4바이트 리틀엔디안

// 정수 읽기
console.log(buf.readUInt8(0));      // 255
console.log(buf.readUInt16BE(1));   // 65535
console.log(buf.readUInt32LE(3));   // 4294967295

// 실수 쓰기/읽기
const floatBuf = Buffer.alloc(8);
floatBuf.writeFloatBE(3.14, 0);
floatBuf.writeDoubleBE(3.141592653589793, 0);

console.log(floatBuf.readDoubleBE(0));  // 3.141592653589793&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Buffer 조작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 버퍼 복사&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const source = Buffer.from('Hello');
const target = Buffer.alloc(10);

source.copy(target);
console.log(target.toString());  // Hello

// 부분 복사
source.copy(target, 5, 0, 3);  // target의 5번 위치에 source의 0~2 복사
console.log(target.toString());  // HelloHel&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 버퍼 연결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const buf1 = Buffer.from('Hello ');
const buf2 = Buffer.from('World');

const combined = Buffer.concat([buf1, buf2]);
console.log(combined.toString());  // Hello World

// 총 길이 지정
const limited = Buffer.concat([buf1, buf2], 8);
console.log(limited.toString());  // Hello Wo&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 버퍼 슬라이스&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const buf = Buffer.from('Hello World');

// slice는 원본과 메모리를 공유
const slice = buf.slice(0, 5);
console.log(slice.toString());  // Hello

slice[0] = 74;  // J로 변경
console.log(buf.toString());  // Jello World (원본도 변경됨)

// subarray도 동일하게 동작
const sub = buf.subarray(6, 11);
console.log(sub.toString());  // World&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 버퍼 비교&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const buf1 = Buffer.from('ABC');
const buf2 = Buffer.from('ABD');
const buf3 = Buffer.from('ABC');

console.log(buf1.compare(buf2));  // -1 (buf1 &amp;lt; buf2)
console.log(buf2.compare(buf1));  // 1 (buf2 &amp;gt; buf1)
console.log(buf1.compare(buf3));  // 0 (같음)

console.log(buf1.equals(buf3));   // true
console.log(buf1.equals(buf2));   // false&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Buffer 정보 확인&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const buf = Buffer.from('Hello');

console.log(buf.length);           // 5 (바이트 수)
console.log(Buffer.byteLength('안녕'));  // 6 (UTF-8에서 한글은 3바이트)
console.log(Buffer.isBuffer(buf)); // true
console.log(Buffer.isEncoding('utf8'));  // true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 파일 바이너리 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 이미지 파일 읽기
const imageBuffer = fs.readFileSync('image.png');
console.log('파일 크기:', imageBuffer.length, 'bytes');

// PNG 시그니처 확인
const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47]);
const isValidPng = imageBuffer.slice(0, 4).equals(pngSignature);
console.log('유효한 PNG:', isValidPng);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Base64 인코딩/디코딩&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;// 문자열 &amp;rarr; Base64
const text = 'Hello, World!';
const base64 = Buffer.from(text).toString('base64');
console.log(base64);  // SGVsbG8sIFdvcmxkIQ==

// Base64 &amp;rarr; 문자열
const decoded = Buffer.from(base64, 'base64').toString('utf8');
console.log(decoded);  // Hello, World!&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 바이너리 프로토콜 파싱&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 간단한 바이너리 메시지 파싱
const message = Buffer.from([
  0x01,       // 버전 (1바이트)
  0x00, 0x0A, // 길이 (2바이트, 빅엔디안)
  0x48, 0x65, 0x6c, 0x6c, 0x6f  // 데이터 &quot;Hello&quot;
]);

const version = message.readUInt8(0);
const length = message.readUInt16BE(1);
const data = message.slice(3, 3 + length).toString();

console.log({ version, length, data });
// { version: 1, length: 10, data: 'Hello' }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Buffer는 Node.js에서 바이너리 데이터를 다루는 핵심 클래스입니다. Buffer.alloc(), Buffer.from()으로 생성하고, toString()으로 문자열 변환, slice()로 부분 추출이 가능합니다. 파일 I/O, 네트워크 통신, 암호화, 이미지 처리 등 바이너리 데이터가 필요한 모든 작업에서 Buffer를 활용합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/794</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%B2%84%ED%8D%BCBuffer-%EC%82%AC%EC%9A%A9%EB%B2%95#entry794comment</comments>
      <pubDate>Tue, 24 Feb 2026 16:00:18 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 스트림 모듈(stream module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EB%AA%A8%EB%93%88stream-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 스트림이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림(Stream)은 데이터를 청크(chunk) 단위로 처리하는 방식입니다. 대용량 파일이나 네트워크 데이터를 전체를 메모리에 로드하지 않고 조각조각 처리할 수 있어 메모리 효율이 높습니다. Node.js의 많은 내장 모듈(http, fs, zlib 등)이 스트림 인터페이스를 사용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스트림의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 네 가지 종류의 스트림을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Readable&lt;/b&gt;: 데이터를 읽을 수 있는 스트림 (fs.createReadStream, http 요청)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Writable&lt;/b&gt;: 데이터를 쓸 수 있는 스트림 (fs.createWriteStream, http 응답)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Duplex&lt;/b&gt;: 읽기/쓰기 모두 가능한 스트림 (TCP 소켓)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transform&lt;/b&gt;: 데이터를 변환하면서 통과시키는 스트림 (zlib 압축)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Readable 스트림&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 파일 읽기 스트림&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('largefile.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024  // 64KB 청크
});

readStream.on('data', (chunk) =&amp;gt; {
  console.log('청크 크기:', chunk.length);
  console.log('청크 내용:', chunk.substring(0, 100));
});

readStream.on('end', () =&amp;gt; {
  console.log('파일 읽기 완료');
});

readStream.on('error', (err) =&amp;gt; {
  console.error('에러:', err.message);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 스트림 일시 정지/재개&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const readStream = fs.createReadStream('file.txt');

readStream.on('data', (chunk) =&amp;gt; {
  console.log('데이터 수신');

  // 일시 정지
  readStream.pause();

  setTimeout(() =&amp;gt; {
    // 재개
    readStream.resume();
  }, 1000);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Writable 스트림&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 파일 쓰기 스트림&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt');

writeStream.write('첫 번째 줄\n');
writeStream.write('두 번째 줄\n');
writeStream.write('세 번째 줄\n');
writeStream.end('마지막 줄');  // end()로 스트림 종료

writeStream.on('finish', () =&amp;gt; {
  console.log('파일 쓰기 완료');
});

writeStream.on('error', (err) =&amp;gt; {
  console.error('에러:', err.message);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 drain 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 버퍼가 가득 찼을 때 drain 이벤트를 활용합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt');

function writeData(data) {
  const canContinue = writeStream.write(data);

  if (!canContinue) {
    console.log('버퍼 가득 참, 대기 중...');
    writeStream.once('drain', () =&amp;gt; {
      console.log('버퍼 비워짐, 계속 쓰기 가능');
    });
  }
}

for (let i = 0; i &amp;lt; 1000000; i++) {
  writeData(`데이터 라인 ${i}\n`);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. pipe() 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림을 연결하는 가장 간단한 방법입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 파일 복사&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('destination.txt');

readStream.pipe(writeStream);

writeStream.on('finish', () =&amp;gt; {
  console.log('파일 복사 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 여러 스트림 연결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs');
const zlib = require('zlib');

// 파일 읽기 &amp;rarr; 압축 &amp;rarr; 파일 쓰기
fs.createReadStream('file.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('file.txt.gz'))
  .on('finish', () =&amp;gt; {
    console.log('압축 완료');
  });

// 압축 해제
fs.createReadStream('file.txt.gz')
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream('file_unzipped.txt'));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 HTTP 응답에 파일 스트리밍&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) =&amp;gt; {
  if (req.url === '/video') {
    const videoPath = 'video.mp4';
    const stat = fs.statSync(videoPath);

    res.writeHead(200, {
      'Content-Type': 'video/mp4',
      'Content-Length': stat.size
    });

    fs.createReadStream(videoPath).pipe(res);
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Transform 스트림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 변환하면서 통과시키는 스트림입니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;const { Transform } = require('stream');

// 대문자 변환 스트림
const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    const upperCased = chunk.toString().toUpperCase();
    callback(null, upperCased);
  }
});

// 사용
process.stdin
  .pipe(upperCaseTransform)
  .pipe(process.stdout);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 커스텀 Transform 스트림&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const { Transform } = require('stream');

class JSONParser extends Transform {
  constructor() {
    super({ objectMode: true });
    this.buffer = '';
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();

    const lines = this.buffer.split('\n');
    this.buffer = lines.pop();  // 마지막 불완전한 줄 보관

    for (const line of lines) {
      if (line.trim()) {
        try {
          const obj = JSON.parse(line);
          this.push(obj);
        } catch (err) {
          callback(err);
          return;
        }
      }
    }

    callback();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. pipeline() 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 10부터 제공되는 안전한 스트림 연결 방식입니다. 에러 처리와 정리를 자동으로 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

pipeline(
  fs.createReadStream('input.txt'),
  zlib.createGzip(),
  fs.createWriteStream('input.txt.gz'),
  (err) =&amp;gt; {
    if (err) {
      console.error('파이프라인 에러:', err);
    } else {
      console.log('파이프라인 완료');
    }
  }
);

// Promise 버전 (Node.js 15+)
const { pipeline } = require('stream/promises');

async function compress() {
  await pipeline(
    fs.createReadStream('input.txt'),
    zlib.createGzip(),
    fs.createWriteStream('input.txt.gz')
  );
  console.log('압축 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 스트림과 일반 방식 비교&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fs = require('fs');

// 일반 방식 - 전체 파일을 메모리에 로드
fs.readFile('largefile.txt', (err, data) =&amp;gt; {
  // data에 전체 파일 내용이 로드됨 (메모리 사용량 높음)
  fs.writeFile('copy.txt', data, () =&amp;gt; {});
});

// 스트림 방식 - 청크 단위로 처리
fs.createReadStream('largefile.txt')
  .pipe(fs.createWriteStream('copy.txt'));
// 청크 단위로 처리되어 메모리 효율적&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림은 대용량 데이터를 메모리 효율적으로 처리하는 Node.js의 핵심 기능입니다. Readable, Writable, Duplex, Transform 네 가지 종류가 있으며, pipe()나 pipeline()으로 스트림을 연결할 수 있습니다. 파일 처리, HTTP 응답, 데이터 압축 등에서 스트림을 활용하면 메모리 사용량을 크게 줄일 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/793</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EB%AA%A8%EB%93%88stream-module#entry793comment</comments>
      <pubDate>Tue, 24 Feb 2026 11:00:04 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 이벤트 모듈(events module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%AA%A8%EB%93%88events-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이벤트 모듈이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;events 모듈은 Node.js에서 이벤트 기반 프로그래밍을 구현하기 위한 핵심 내장 모듈입니다. Node.js의 많은 내장 모듈(http, fs, stream 등)이 이 모듈을 기반으로 동작합니다. 이벤트를 발생시키고(emit) 리스너를 등록하여(on) 비동기 작업을 처리하는 패턴을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. EventEmitter 기본 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 EventEmitter 생성&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;const EventEmitter = require('events');

// 방법 1: 직접 인스턴스 생성
const emitter = new EventEmitter();

// 방법 2: 클래스 상속
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 이벤트 등록과 발생&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const EventEmitter = require('events');
const emitter = new EventEmitter();

// 이벤트 리스너 등록
emitter.on('greeting', (name) =&amp;gt; {
  console.log(`안녕하세요, ${name}님!`);
});

// 이벤트 발생
emitter.emit('greeting', '홍길동');
// 출력: 안녕하세요, 홍길동님!&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이벤트 리스너 메서드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 on() - 이벤트 리스너 등록&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// 여러 개의 리스너 등록 가능
emitter.on('data', (data) =&amp;gt; {
  console.log('리스너 1:', data);
});

emitter.on('data', (data) =&amp;gt; {
  console.log('리스너 2:', data);
});

emitter.emit('data', '테스트');
// 출력:
// 리스너 1: 테스트
// 리스너 2: 테스트&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 once() - 일회성 리스너&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

emitter.once('connect', () =&amp;gt; {
  console.log('연결됨 (한 번만 실행)');
});

emitter.emit('connect');  // 연결됨 (한 번만 실행)
emitter.emit('connect');  // 아무 출력 없음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 off() / removeListener() - 리스너 제거&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

function handler(data) {
  console.log('데이터:', data);
}

emitter.on('data', handler);
emitter.emit('data', '첫 번째');  // 데이터: 첫 번째

emitter.off('data', handler);
// 또는 emitter.removeListener('data', handler);

emitter.emit('data', '두 번째');  // 아무 출력 없음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 removeAllListeners() - 모든 리스너 제거&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

emitter.on('event', () =&amp;gt; console.log('리스너 1'));
emitter.on('event', () =&amp;gt; console.log('리스너 2'));

// 특정 이벤트의 모든 리스너 제거
emitter.removeAllListeners('event');

// 모든 이벤트의 모든 리스너 제거
emitter.removeAllListeners();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 이벤트 정보 확인&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

emitter.on('data', () =&amp;gt; {});
emitter.on('data', () =&amp;gt; {});
emitter.on('error', () =&amp;gt; {});

// 등록된 이벤트 이름 목록
console.log(emitter.eventNames());
// ['data', 'error']

// 특정 이벤트의 리스너 수
console.log(emitter.listenerCount('data'));
// 2

// 특정 이벤트의 리스너 배열
console.log(emitter.listeners('data'));
// [Function, Function]&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 에러 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;error 이벤트는 특별하게 처리됩니다. 리스너가 없으면 예외가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// error 이벤트 리스너가 없으면 프로세스가 종료됨
// emitter.emit('error', new Error('문제 발생')); // 예외 발생!

// error 이벤트 리스너 등록
emitter.on('error', (err) =&amp;gt; {
  console.error('에러 발생:', err.message);
});

emitter.emit('error', new Error('문제 발생'));
// 에러 발생: 문제 발생&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 최대 리스너 수 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 누수 방지를 위해 기본적으로 이벤트당 10개의 리스너 제한이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// 기본값 확인
console.log(emitter.getMaxListeners());  // 10

// 최대 리스너 수 변경
emitter.setMaxListeners(20);

// 전역 기본값 변경
EventEmitter.defaultMaxListeners = 15;

// 무제한으로 설정 (권장하지 않음)
emitter.setMaxListeners(0);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 비동기 이벤트 처리&lt;/h2&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const emitter = new EventEmitter();

// 비동기 리스너
emitter.on('fetch', async (url) =&amp;gt; {
  try {
    const response = await fetch(url);
    const data = await response.json();
    emitter.emit('data', data);
  } catch (err) {
    emitter.emit('error', err);
  }
});

emitter.on('data', (data) =&amp;gt; {
  console.log('받은 데이터:', data);
});

emitter.on('error', (err) =&amp;gt; {
  console.error('에러:', err.message);
});

emitter.emit('fetch', 'https://api.example.com/data');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 커스텀 클래스에서 이벤트 사용&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const EventEmitter = require('events');

class Database extends EventEmitter {
  constructor() {
    super();
    this.connected = false;
  }

  connect() {
    setTimeout(() =&amp;gt; {
      this.connected = true;
      this.emit('connected');
    }, 1000);
  }

  query(sql) {
    if (!this.connected) {
      this.emit('error', new Error('데이터베이스 연결 안됨'));
      return;
    }

    setTimeout(() =&amp;gt; {
      this.emit('result', { sql, rows: [] });
    }, 500);
  }

  disconnect() {
    this.connected = false;
    this.emit('disconnected');
  }
}

// 사용
const db = new Database();

db.on('connected', () =&amp;gt; console.log('DB 연결됨'));
db.on('result', (data) =&amp;gt; console.log('쿼리 결과:', data));
db.on('error', (err) =&amp;gt; console.error('에러:', err.message));
db.on('disconnected', () =&amp;gt; console.log('DB 연결 해제'));

db.connect();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;events 모듈의 EventEmitter는 Node.js 이벤트 기반 아키텍처의 핵심입니다. on()으로 리스너를 등록하고 emit()으로 이벤트를 발생시키는 패턴을 사용합니다. error 이벤트는 반드시 리스너를 등록해야 하며, 클래스 상속을 통해 커스텀 이벤트 기반 객체를 만들 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/792</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%AA%A8%EB%93%88events-module#entry792comment</comments>
      <pubDate>Tue, 24 Feb 2026 08:00:51 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 경로 모듈(path module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EB%AA%A8%EB%93%88path-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. path 모듈이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path 모듈은 파일과 디렉토리 경로를 다루기 위한 Node.js 내장 모듈입니다. 운영체제마다 경로 구분자가 다른데(Windows는 , POSIX는 /), path 모듈을 사용하면 운영체제에 관계없이 일관된 방식으로 경로를 처리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. path 모듈 불러오기&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// CommonJS
const path = require('path');

// ES Modules
import path from 'node:path';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 경로 정보 추출&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 path.basename()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로에서 파일명을 추출합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.basename('/home/user/file.txt'));
// file.txt

console.log(path.basename('/home/user/file.txt', '.txt'));
// file (확장자 제외)

console.log(path.basename('/home/user/'));
// user&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 path.dirname()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로에서 디렉토리 경로를 추출합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.dirname('/home/user/file.txt'));
// /home/user

console.log(path.dirname('/home/user/docs/'));
// /home/user&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 path.extname()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 확장자를 추출합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.extname('file.txt'));       // .txt
console.log(path.extname('file.config.js')); // .js
console.log(path.extname('file'));           // '' (빈 문자열)
console.log(path.extname('.gitignore'));     // '' (빈 문자열)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 path.parse()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 구성 요소로 분해합니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const path = require('path');

const parsed = path.parse('/home/user/docs/file.txt');
console.log(parsed);
/*
{
  root: '/',
  dir: '/home/user/docs',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}
*/

// Windows 경로
const parsedWin = path.parse('C:\\Users\\user\\file.txt');
console.log(parsedWin);
/*
{
  root: 'C:\\',
  dir: 'C:\\Users\\user',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}
*/&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 경로 생성 및 조합&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 path.join()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 경로 세그먼트를 하나의 경로로 결합합니다. 자동으로 구분자를 처리하고 정규화합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.join('/home', 'user', 'docs', 'file.txt'));
// /home/user/docs/file.txt

console.log(path.join('/home', 'user', '..', 'docs'));
// /home/docs (..은 상위 디렉토리로 이동)

console.log(path.join('folder', 'subfolder', 'file.txt'));
// folder/subfolder/file.txt

// 빈 문자열이나 현재 디렉토리 처리
console.log(path.join('/home', '', 'user'));
// /home/user&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 path.resolve()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대 경로를 생성합니다. 오른쪽에서 왼쪽으로 경로를 처리하며, 절대 경로가 만들어지면 멈춥니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

// 현재 작업 디렉토리 기준 절대 경로 생성
console.log(path.resolve('file.txt'));
// /현재/작업/디렉토리/file.txt

console.log(path.resolve('/home', 'user', 'file.txt'));
// /home/user/file.txt

console.log(path.resolve('/home', '/user', 'file.txt'));
// /user/file.txt (중간에 절대 경로가 있으면 그 이전은 무시)

// __dirname과 함께 사용
console.log(path.resolve(__dirname, 'config', 'settings.json'));
// /현재/파일/위치/config/settings.json&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 path.format()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parse()의 반대로, 객체를 경로 문자열로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const path = require('path');

const pathString = path.format({
  dir: '/home/user',
  base: 'file.txt'
});
console.log(pathString);  // /home/user/file.txt

const pathString2 = path.format({
  root: '/',
  name: 'file',
  ext: '.txt'
});
console.log(pathString2);  // /file.txt&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 경로 변환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 path.normalize()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 정규화합니다. 불필요한 . 이나 .., 중복 구분자를 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.normalize('/home//user/../docs/./file.txt'));
// /home/docs/file.txt

console.log(path.normalize('folder/subfolder/../file.txt'));
// folder/file.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 path.relative()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 경로에서 다른 경로로의 상대 경로를 계산합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.relative('/home/user/docs', '/home/user/images'));
// ../images

console.log(path.relative('/home/user', '/home/user/docs/file.txt'));
// docs/file.txt

console.log(path.relative('/home/user/docs', '/home/user/docs'));
// '' (빈 문자열 - 같은 경로)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 path.isAbsolute()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로가 절대 경로인지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;const path = require('path');

// POSIX
console.log(path.isAbsolute('/home/user'));  // true
console.log(path.isAbsolute('./file.txt'));  // false
console.log(path.isAbsolute('file.txt'));    // false

// Windows
console.log(path.isAbsolute('C:\\Users'));   // true
console.log(path.isAbsolute('\\Users'));     // true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 플랫폼별 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 path.sep&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제의 경로 구분자입니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.sep);
// POSIX: /
// Windows: \

// 경로를 배열로 분리
const filePath = '/home/user/docs/file.txt';
const segments = filePath.split(path.sep);
console.log(segments);  // ['', 'home', 'user', 'docs', 'file.txt']&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 path.delimiter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수 PATH의 구분자입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const path = require('path');

console.log(path.delimiter);
// POSIX: :
// Windows: ;

// PATH 환경 변수 분리
console.log(process.env.PATH.split(path.delimiter));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 플랫폼별 명시적 사용&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;const path = require('path');

// POSIX 스타일 강제
console.log(path.posix.join('home', 'user'));  // home/user

// Windows 스타일 강제
console.log(path.win32.join('home', 'user'));  // home\user&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 __dirname, __filename 활용&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;const path = require('path');

// 현재 파일의 절대 경로
console.log(__filename);
// /home/user/project/app.js

// 현재 파일의 디렉토리 경로
console.log(__dirname);
// /home/user/project

// 상대 경로로 파일 찾기
const configPath = path.join(__dirname, 'config', 'settings.json');
const publicPath = path.resolve(__dirname, '..', 'public');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 파일 확장자에 따른 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const path = require('path');
const fs = require('fs').promises;

async function processFiles(directory) {
  const files = await fs.readdir(directory);

  for (const file of files) {
    const ext = path.extname(file).toLowerCase();
    const filePath = path.join(directory, file);

    switch (ext) {
      case '.json':
        console.log('JSON 파일:', file);
        break;
      case '.js':
        console.log('JavaScript 파일:', file);
        break;
      case '.md':
        console.log('Markdown 파일:', file);
        break;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 안전한 파일 경로 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const path = require('path');

function getSafeFilePath(baseDir, userInput) {
  // 사용자 입력에서 경로 조작 방지
  const fileName = path.basename(userInput);
  const safePath = path.join(baseDir, fileName);

  // 결과 경로가 baseDir 내에 있는지 확인
  const resolvedPath = path.resolve(safePath);
  const resolvedBase = path.resolve(baseDir);

  if (!resolvedPath.startsWith(resolvedBase)) {
    throw new Error('잘못된 파일 경로');
  }

  return resolvedPath;
}

// 사용
const safePath = getSafeFilePath('/uploads', '../../../etc/passwd');
// /uploads/passwd (경로 조작 방지됨)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 path 모듈은 파일 경로를 안전하고 일관되게 다루기 위한 필수 도구입니다. path.join()과 path.resolve()로 경로를 조합하고, path.parse()로 경로 정보를 추출할 수 있습니다. 운영체제에 따른 경로 구분자 차이를 자동으로 처리하므로, 크로스 플랫폼 애플리케이션 개발에 필수적입니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/791</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EB%AA%A8%EB%93%88path-module#entry791comment</comments>
      <pubDate>Mon, 23 Feb 2026 21:00:38 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 URL 모듈(url module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-URL-%EB%AA%A8%EB%93%88url-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. URL 모듈이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url 모듈은 URL 문자열을 파싱하고 조작하기 위한 Node.js 내장 모듈입니다. URL의 각 구성 요소(프로토콜, 호스트, 경로, 쿼리 파라미터 등)를 분리하거나 조합할 수 있습니다. Node.js는 레거시 API와 WHATWG URL API 두 가지를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. URL의 구조&lt;/h2&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;https://user:pass@www.example.com:8080/path/page?query=value#hash
  │       │    │          │        │      │           │       │
  │       │    │          │        │      │           │       └─ hash (fragment)
  │       │    │          │        │      │           └─ search (query string)
  │       │    │          │        │      └─ pathname
  │       │    │          │        └─ port
  │       │    │          └─ hostname
  │       │    └─ password
  │       └─ username
  └─ protocol&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. WHATWG URL API (권장)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 7.0 이상에서 사용 가능한 최신 표준 API입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 URL 파싱&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { URL } = require('url');

const myUrl = new URL('https://www.example.com:8080/path/page?name=홍길동&amp;amp;age=30#section1');

console.log('전체 URL:', myUrl.href);
console.log('프로토콜:', myUrl.protocol);    // https:
console.log('호스트:', myUrl.host);          // www.example.com:8080
console.log('호스트명:', myUrl.hostname);    // www.example.com
console.log('포트:', myUrl.port);            // 8080
console.log('경로:', myUrl.pathname);        // /path/page
console.log('쿼리:', myUrl.search);          // ?name=홍길동&amp;amp;age=30
console.log('해시:', myUrl.hash);            // #section1
console.log('origin:', myUrl.origin);        // https://www.example.com:8080&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 쿼리 파라미터 다루기 (URLSearchParams)&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const { URL } = require('url');

const myUrl = new URL('https://example.com/search?q=nodejs&amp;amp;page=1');
const params = myUrl.searchParams;

// 파라미터 읽기
console.log(params.get('q'));      // nodejs
console.log(params.get('page'));   // 1
console.log(params.get('none'));   // null

// 파라미터 존재 여부 확인
console.log(params.has('q'));      // true

// 모든 파라미터 순회
for (const [key, value] of params) {
  console.log(`${key}: ${value}`);
}

// 파라미터 추가/수정/삭제
params.append('category', 'tech');
params.set('page', '2');
params.delete('q');

console.log(myUrl.href);
// https://example.com/search?page=2&amp;amp;category=tech&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 URL 생성 및 수정&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const { URL } = require('url');

// 기본 URL로 새 URL 생성
const base = 'https://example.com';
const myUrl = new URL('/users/123', base);
console.log(myUrl.href);  // https://example.com/users/123

// URL 속성 수정
myUrl.pathname = '/products/456';
myUrl.searchParams.set('sort', 'price');
myUrl.hash = 'reviews';

console.log(myUrl.href);
// https://example.com/products/456?sort=price#reviews&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 레거시 URL API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 전통적인 URL 파싱 방식입니다. 하위 호환성을 위해 유지되고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 url.parse()&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const url = require('url');

const myUrl = url.parse('https://www.example.com:8080/path?name=test#hash', true);

console.log(myUrl.protocol);  // https:
console.log(myUrl.host);      // www.example.com:8080
console.log(myUrl.hostname);  // www.example.com
console.log(myUrl.port);      // 8080
console.log(myUrl.pathname);  // /path
console.log(myUrl.search);    // ?name=test
console.log(myUrl.query);     // { name: 'test' } (두 번째 인자가 true일 때)
console.log(myUrl.hash);      // #hash&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 url.format()&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const url = require('url');

const urlObject = {
  protocol: 'https:',
  hostname: 'example.com',
  port: 8080,
  pathname: '/path/to/page',
  query: { name: 'test', value: '123' }
};

const urlString = url.format(urlObject);
console.log(urlString);
// https://example.com:8080/path/to/page?name=test&amp;amp;value=123&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 url.resolve()&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const url = require('url');

console.log(url.resolve('https://example.com/a/b', '/c'));
// https://example.com/c

console.log(url.resolve('https://example.com/a/b', 'c'));
// https://example.com/a/c

console.log(url.resolve('https://example.com/a/b/', 'c'));
// https://example.com/a/b/c&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 HTTP 서버에서 URL 파싱&lt;/h3&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;const http = require('http');
const { URL } = require('url');

const server = http.createServer((req, res) =&amp;gt; {
  // 전체 URL 생성
  const baseUrl = `http://${req.headers.host}`;
  const url = new URL(req.url, baseUrl);

  console.log('경로:', url.pathname);
  console.log('쿼리 파라미터:', Object.fromEntries(url.searchParams));

  // 라우팅
  if (url.pathname === '/api/users') {
    const page = url.searchParams.get('page') || '1';
    const limit = url.searchParams.get('limit') || '10';

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ page, limit }));
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 API URL 빌더&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const { URL } = require('url');

function buildApiUrl(baseUrl, endpoint, params = {}) {
  const url = new URL(endpoint, baseUrl);

  Object.entries(params).forEach(([key, value]) =&amp;gt; {
    if (value !== undefined &amp;amp;&amp;amp; value !== null) {
      url.searchParams.append(key, value);
    }
  });

  return url.href;
}

// 사용
const apiUrl = buildApiUrl(
  'https://api.example.com',
  '/v1/users',
  { page: 1, limit: 20, sort: 'name' }
);

console.log(apiUrl);
// https://api.example.com/v1/users?page=1&amp;amp;limit=20&amp;amp;sort=name&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 URL 유효성 검사&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function isValidUrl(string) {
  try {
    new URL(string);
    return true;
  } catch (err) {
    return false;
  }
}

console.log(isValidUrl('https://example.com'));  // true
console.log(isValidUrl('not-a-url'));            // false
console.log(isValidUrl('/path/to/page'));        // false (상대 경로)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 URL 파라미터 인코딩/디코딩&lt;/h3&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// URLSearchParams는 자동으로 인코딩/디코딩 처리
const params = new URLSearchParams();
params.set('name', '홍길동');
params.set('message', 'Hello World!');

console.log(params.toString());
// name=%ED%99%8D%EA%B8%B8%EB%8F%99&amp;amp;message=Hello+World%21

// 개별 인코딩/디코딩
console.log(encodeURIComponent('홍길동'));  // %ED%99%8D%EA%B8%B8%EB%8F%99
console.log(decodeURIComponent('%ED%99%8D%EA%B8%B8%EB%8F%99'));  // 홍길동&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 url 모듈은 URL을 파싱하고 조작하기 위한 도구를 제공합니다. WHATWG URL API(new URL())가 최신 표준이며 권장되는 방식입니다. URLSearchParams를 통해 쿼리 파라미터를 쉽게 다룰 수 있고, 자동으로 인코딩/디코딩을 처리합니다. HTTP 서버에서 요청 URL을 파싱하거나 API 클라이언트에서 URL을 생성할 때 유용하게 활용됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/790</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-URL-%EB%AA%A8%EB%93%88url-module#entry790comment</comments>
      <pubDate>Mon, 23 Feb 2026 16:00:21 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 HTTP 모듈(http module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%EB%AA%A8%EB%93%88http-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTTP 모듈이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 모듈은 Node.js에서 HTTP 서버와 클라이언트를 생성하기 위한 내장 모듈입니다. 별도의 웹 프레임워크 없이도 HTTP 서버를 구축할 수 있으며, Express.js 같은 프레임워크도 내부적으로 이 모듈을 기반으로 동작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HTTP 서버 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 서버&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('안녕하세요, Node.js HTTP 서버입니다!');
});

server.listen(3000, () =&amp;gt; {
  console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 요청 정보 확인&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  console.log('요청 메서드:', req.method);
  console.log('요청 URL:', req.url);
  console.log('요청 헤더:', req.headers);
  console.log('User-Agent:', req.headers['user-agent']);

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    method: req.method,
    url: req.url
  }));
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 라우팅 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 기본 라우팅&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  const { method, url } = req;

  // Content-Type 설정
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  if (method === 'GET' &amp;amp;&amp;amp; url === '/') {
    res.writeHead(200);
    res.end(JSON.stringify({ message: '홈페이지' }));
  } else if (method === 'GET' &amp;amp;&amp;amp; url === '/users') {
    res.writeHead(200);
    res.end(JSON.stringify({ users: ['홍길동', '김철수'] }));
  } else if (method === 'POST' &amp;amp;&amp;amp; url === '/users') {
    res.writeHead(201);
    res.end(JSON.stringify({ message: '사용자 생성됨' }));
  } else {
    res.writeHead(404);
    res.end(JSON.stringify({ error: 'Not Found' }));
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 URL 파라미터 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const http = require('http');
const url = require('url');

const server = http.createServer((req, res) =&amp;gt; {
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;
  const query = parsedUrl.query;

  res.setHeader('Content-Type', 'application/json');

  // /users/123 형태의 URL 처리
  const userMatch = pathname.match(/^\/users\/(\d+)$/);
  if (userMatch) {
    const userId = userMatch[1];
    res.end(JSON.stringify({ userId }));
    return;
  }

  // /search?q=keyword 형태의 쿼리 파라미터 처리
  if (pathname === '/search') {
    res.end(JSON.stringify({ keyword: query.q }));
    return;
  }

  res.writeHead(404);
  res.end(JSON.stringify({ error: 'Not Found' }));
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 요청 본문 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 POST 요청 데이터 받기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  if (req.method === 'POST' &amp;amp;&amp;amp; req.url === '/users') {
    let body = '';

    // 데이터를 청크 단위로 수신
    req.on('data', (chunk) =&amp;gt; {
      body += chunk.toString();
    });

    // 모든 데이터 수신 완료
    req.on('end', () =&amp;gt; {
      try {
        const data = JSON.parse(body);
        console.log('받은 데이터:', data);

        res.writeHead(201, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
          message: '사용자 생성됨',
          user: data
        }));
      } catch (err) {
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: '잘못된 JSON 형식' }));
      }
    });
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 프로미스로 요청 본문 처리&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function getRequestBody(req) {
  return new Promise((resolve, reject) =&amp;gt; {
    let body = '';
    req.on('data', chunk =&amp;gt; body += chunk.toString());
    req.on('end', () =&amp;gt; {
      try {
        resolve(JSON.parse(body));
      } catch (err) {
        reject(err);
      }
    });
    req.on('error', reject);
  });
}

const server = http.createServer(async (req, res) =&amp;gt; {
  if (req.method === 'POST') {
    try {
      const data = await getRequestBody(req);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ received: data }));
    } catch (err) {
      res.writeHead(400);
      res.end('Bad Request');
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정적 파일 제공&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');
const fs = require('fs');
const path = require('path');

const MIME_TYPES = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'text/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.gif': 'image/gif'
};

const server = http.createServer((req, res) =&amp;gt; {
  let filePath = path.join(__dirname, 'public', req.url);

  // 기본 파일 설정
  if (req.url === '/') {
    filePath = path.join(__dirname, 'public', 'index.html');
  }

  const ext = path.extname(filePath);
  const contentType = MIME_TYPES[ext] || 'application/octet-stream';

  fs.readFile(filePath, (err, content) =&amp;gt; {
    if (err) {
      if (err.code === 'ENOENT') {
        res.writeHead(404);
        res.end('File Not Found');
      } else {
        res.writeHead(500);
        res.end('Server Error');
      }
      return;
    }

    res.writeHead(200, { 'Content-Type': contentType });
    res.end(content);
  });
});

server.listen(3000);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. HTTP 클라이언트 요청&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 GET 요청&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const http = require('http');

const options = {
  hostname: 'jsonplaceholder.typicode.com',
  path: '/posts/1',
  method: 'GET'
};

const req = http.request(options, (res) =&amp;gt; {
  let data = '';

  res.on('data', (chunk) =&amp;gt; {
    data += chunk;
  });

  res.on('end', () =&amp;gt; {
    console.log('응답:', JSON.parse(data));
  });
});

req.on('error', (err) =&amp;gt; {
  console.error('에러:', err.message);
});

req.end();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 POST 요청&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');

const postData = JSON.stringify({
  title: '새 글',
  body: '내용입니다',
  userId: 1
});

const options = {
  hostname: 'jsonplaceholder.typicode.com',
  path: '/posts',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(postData)
  }
};

const req = http.request(options, (res) =&amp;gt; {
  let data = '';
  res.on('data', chunk =&amp;gt; data += chunk);
  res.on('end', () =&amp;gt; console.log('응답:', JSON.parse(data)));
});

req.write(postData);
req.end();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 간단한 GET 요청 (http.get)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const http = require('http');

http.get('http://jsonplaceholder.typicode.com/posts/1', (res) =&amp;gt; {
  let data = '';
  res.on('data', chunk =&amp;gt; data += chunk);
  res.on('end', () =&amp;gt; console.log(JSON.parse(data)));
}).on('error', err =&amp;gt; console.error(err));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. HTTPS 서버&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem')
};

const server = https.createServer(options, (req, res) =&amp;gt; {
  res.writeHead(200);
  res.end('HTTPS 서버입니다');
});

server.listen(443, () =&amp;gt; {
  console.log('HTTPS 서버 실행 중');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 http 모듈은 HTTP 서버와 클라이언트를 구현하기 위한 기본 도구입니다. createServer로 서버를 생성하고, 요청 객체(req)와 응답 객체(res)를 통해 HTTP 통신을 처리합니다. 실제 프로덕션에서는 Express.js 같은 프레임워크를 사용하는 것이 일반적이지만, http 모듈의 동작 원리를 이해하면 프레임워크를 더 효과적으로 활용할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/789</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-HTTP-%EB%AA%A8%EB%93%88http-module#entry789comment</comments>
      <pubDate>Mon, 23 Feb 2026 11:00:55 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 파일 시스템 모듈(fs module)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%AA%A8%EB%93%88fs-module</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. fs 모듈이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fs(File System) 모듈은 Node.js에서 파일과 디렉토리를 다루기 위한 내장 모듈입니다. 파일 읽기, 쓰기, 삭제, 디렉토리 생성 등의 작업을 수행할 수 있으며, 동기/비동기/프로미스 세 가지 방식의 API를 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. fs 모듈 불러오기&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// CommonJS
const fs = require('fs');
const fsPromises = require('fs').promises;
// 또는
const { readFile, writeFile } = require('fs/promises');

// ES Modules
import fs from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파일 읽기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 비동기 방식 (콜백)&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) =&amp;gt; {
  if (err) {
    console.error('파일 읽기 실패:', err.message);
    return;
  }
  console.log('파일 내용:', data);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 동기 방식&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

try {
  const data = fs.readFileSync('example.txt', 'utf8');
  console.log('파일 내용:', data);
} catch (err) {
  console.error('파일 읽기 실패:', err.message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 프로미스 방식&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function readFileAsync() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log('파일 내용:', data);
  } catch (err) {
    console.error('파일 읽기 실패:', err.message);
  }
}

readFileAsync();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파일 쓰기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 새 파일 작성 (덮어쓰기)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function writeToFile() {
  const content = '안녕하세요, Node.js!';

  await fs.writeFile('output.txt', content, 'utf8');
  console.log('파일 작성 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 파일에 내용 추가&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function appendToFile() {
  const content = '\n추가된 내용';

  await fs.appendFile('output.txt', content, 'utf8');
  console.log('내용 추가 완료');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 JSON 파일 읽기/쓰기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// JSON 읽기
async function readJSON(filePath) {
  const data = await fs.readFile(filePath, 'utf8');
  return JSON.parse(data);
}

// JSON 쓰기
async function writeJSON(filePath, data) {
  const jsonString = JSON.stringify(data, null, 2);
  await fs.writeFile(filePath, jsonString, 'utf8');
}

// 사용
async function main() {
  const config = await readJSON('config.json');
  config.version = '2.0.0';
  await writeJSON('config.json', config);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파일/디렉토리 정보 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 파일 존재 여부 확인&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function checkFileExists(filePath) {
  try {
    await fs.access(filePath);
    return true;
  } catch {
    return false;
  }
}

// 사용
const exists = await checkFileExists('example.txt');
console.log('파일 존재:', exists);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 파일/디렉토리 정보 (stat)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function getFileInfo(filePath) {
  const stats = await fs.stat(filePath);

  console.log('파일 크기:', stats.size, 'bytes');
  console.log('생성일:', stats.birthtime);
  console.log('수정일:', stats.mtime);
  console.log('디렉토리인가:', stats.isDirectory());
  console.log('파일인가:', stats.isFile());
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 디렉토리 작업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 디렉토리 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 단일 디렉토리 생성
await fs.mkdir('newFolder');

// 중첩 디렉토리 생성 (recursive 옵션)
await fs.mkdir('path/to/nested/folder', { recursive: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 디렉토리 내용 읽기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function listDirectory(dirPath) {
  // 기본 - 파일명 배열 반환
  const files = await fs.readdir(dirPath);
  console.log(files); // ['file1.txt', 'file2.txt', 'folder1']

  // withFileTypes 옵션 - Dirent 객체 배열 반환
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  for (const entry of entries) {
    const type = entry.isDirectory() ? ' ' : ' ';
    console.log(`${type} ${entry.name}`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 디렉토리 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 빈 디렉토리 삭제
await fs.rmdir('emptyFolder');

// 내용이 있는 디렉토리 삭제 (recursive 옵션)
await fs.rm('folderWithContents', { recursive: true });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 파일 삭제 및 이름 변경&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 파일 삭제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

await fs.unlink('fileToDelete.txt');
// 또는
await fs.rm('fileToDelete.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 파일/디렉토리 이름 변경&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// 이름 변경
await fs.rename('oldName.txt', 'newName.txt');

// 파일 이동
await fs.rename('file.txt', 'folder/file.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 파일 복사&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

await fs.copyFile('source.txt', 'destination.txt');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 스트림을 이용한 대용량 파일 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 파일은 스트림을 사용하여 메모리 효율적으로 처리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 읽기 스트림&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('largefile.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024 // 64KB 청크
});

readStream.on('data', (chunk) =&amp;gt; {
  console.log('청크 크기:', chunk.length);
});

readStream.on('end', () =&amp;gt; {
  console.log('파일 읽기 완료');
});

readStream.on('error', (err) =&amp;gt; {
  console.error('에러:', err.message);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 쓰기 스트림&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const writeStream = fs.createWriteStream('output.txt');

writeStream.write('첫 번째 줄\n');
writeStream.write('두 번째 줄\n');
writeStream.end('마지막 줄');

writeStream.on('finish', () =&amp;gt; {
  console.log('파일 쓰기 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 파이프로 파일 복사&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;const fs = require('fs');

const readStream = fs.createReadStream('source.txt');
const writeStream = fs.createWriteStream('destination.txt');

readStream.pipe(writeStream);

writeStream.on('finish', () =&amp;gt; {
  console.log('파일 복사 완료');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 파일 감시 (watch)&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// 파일 변경 감시
fs.watch('watched.txt', (eventType, filename) =&amp;gt; {
  console.log(`이벤트: ${eventType}, 파일: ${filename}`);
});

// 디렉토리 감시 (recursive 옵션)
fs.watch('watchedFolder', { recursive: true }, (eventType, filename) =&amp;gt; {
  console.log(`이벤트: ${eventType}, 파일: ${filename}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 fs 모듈은 파일과 디렉토리를 다루는 다양한 API를 제공합니다. 프로미스 기반의 fs/promises를 사용하면 async/await로 깔끔하게 코드를 작성할 수 있습니다. 대용량 파일은 스트림을 사용하여 메모리 효율적으로 처리하고, 동기 방식은 서버 시작 시 설정 파일 로드 등 특수한 경우에만 사용하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/788</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%8C%8C%EC%9D%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%AA%A8%EB%93%88fs-module#entry788comment</comments>
      <pubDate>Mon, 23 Feb 2026 08:00:37 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 모듈 시스템</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 모듈 시스템이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 시스템은 코드를 여러 파일로 분리하고 재사용할 수 있게 해주는 구조입니다. Node.js는 CommonJS와 ES Modules 두 가지 모듈 시스템을 지원합니다. 모듈화를 통해 코드의 유지보수성, 재사용성, 캡슐화를 향상시킬 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. CommonJS (CJS)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 기본 모듈 시스템으로, require()와 module.exports를 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 모듈 내보내기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// math.js

// 방법 1: module.exports에 객체 할당
module.exports = {
  add: (a, b) =&amp;gt; a + b,
  subtract: (a, b) =&amp;gt; a - b
};

// 방법 2: exports에 속성 추가
exports.multiply = (a, b) =&amp;gt; a * b;
exports.divide = (a, b) =&amp;gt; a / b;

// 방법 3: 단일 함수/클래스 내보내기
module.exports = function Calculator() {
  this.value = 0;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 모듈 가져오기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// app.js

// 전체 모듈 가져오기
const math = require('./math');
console.log(math.add(2, 3)); // 5

// 구조 분해 할당으로 가져오기
const { add, subtract } = require('./math');
console.log(add(2, 3)); // 5

// 내장 모듈 가져오기
const fs = require('fs');
const path = require('path');

// npm 패키지 가져오기
const express = require('express');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 module.exports vs exports&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// exports는 module.exports의 참조
console.log(exports === module.exports); // true

// exports에 직접 할당하면 참조가 끊어짐 (작동 안 함)
exports = { name: 'test' }; // 잘못된 방법

// module.exports에 할당해야 함
module.exports = { name: 'test' }; // 올바른 방법&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ES Modules (ESM)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6에서 도입된 표준 JavaScript 모듈 시스템입니다. Node.js 12버전부터 안정적으로 지원됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 ESM 활성화 방법&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// package.json
{
  &quot;type&quot;: &quot;module&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 파일 확장자를 .mjs로 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 모듈 내보내기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// utils.mjs 또는 utils.js (type: module 설정 시)

// Named Export (이름 있는 내보내기)
export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

export class Calculator {
  constructor() {
    this.value = 0;
  }
}

// Default Export (기본 내보내기)
export default function main() {
  console.log('메인 함수');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 모듈 가져오기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// app.mjs

// Named Import
import { add, PI } from './utils.mjs';

// Default Import
import main from './utils.mjs';

// 모든 것을 객체로 가져오기
import * as utils from './utils.mjs';
console.log(utils.add(2, 3));

// Named + Default 함께 가져오기
import main, { add, PI } from './utils.mjs';

// 이름 변경하여 가져오기
import { add as sum } from './utils.mjs';&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CommonJS vs ES Modules 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;CommonJS&lt;/th&gt;
&lt;th&gt;ES Modules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;문법&lt;/td&gt;
&lt;td&gt;require() / module.exports&lt;/td&gt;
&lt;td&gt;import / export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로딩&lt;/td&gt;
&lt;td&gt;동기적&lt;/td&gt;
&lt;td&gt;비동기적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 확장자&lt;/td&gt;
&lt;td&gt;.js, .cjs&lt;/td&gt;
&lt;td&gt;.mjs 또는 type: module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top-level await&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;this&lt;/td&gt;
&lt;td&gt;module.exports&lt;/td&gt;
&lt;td&gt;undefined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동적 import&lt;/td&gt;
&lt;td&gt;require() 어디서든 가능&lt;/td&gt;
&lt;td&gt;import() 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 동적 Import&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 CommonJS 동적 로딩&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 조건부 로딩
if (condition) {
  const module = require('./moduleA');
} else {
  const module = require('./moduleB');
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 ESM 동적 로딩&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// import()는 프로미스를 반환
async function loadModule() {
  if (condition) {
    const module = await import('./moduleA.mjs');
    module.default();
  }
}

// Top-level await (ESM에서만 가능)
const config = await import('./config.mjs');&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 내장 모듈 vs 외부 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 내장 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에 기본으로 포함된 모듈입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// CommonJS
const fs = require('fs');
const path = require('path');
const http = require('http');
const crypto = require('crypto');

// ESM
import fs from 'fs';
import path from 'path';
// 또는 node: 프로토콜 사용 (권장)
import fs from 'node:fs';
import path from 'node:path';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 외부 모듈 (npm 패키지)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm을 통해 설치한 모듈입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 설치
// npm install express lodash

// 사용
const express = require('express');
import _ from 'lodash';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 모듈 캐싱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 모듈을 한 번 로드하면 캐시에 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// moduleA.js
console.log('모듈 A 로드됨');
module.exports = { value: 1 };

// app.js
const a1 = require('./moduleA'); // '모듈 A 로드됨' 출력
const a2 = require('./moduleA'); // 출력 없음 (캐시에서 로드)

console.log(a1 === a2); // true

// 캐시 확인
console.log(require.cache);

// 캐시 삭제 (재로드 필요시)
delete require.cache[require.resolve('./moduleA')];&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 순환 참조 처리&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// a.js
console.log('a.js 시작');
exports.done = false;
const b = require('./b');
console.log('b.done:', b.done);
exports.done = true;
console.log('a.js 완료');

// b.js
console.log('b.js 시작');
exports.done = false;
const a = require('./a');
console.log('a.done:', a.done); // false (a.js가 아직 완료되지 않음)
exports.done = true;
console.log('b.js 완료');

// main.js
require('./a');
// 출력:
// a.js 시작
// b.js 시작
// a.done: false
// b.js 완료
// b.done: true
// a.js 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조는 가능하면 피하는 것이 좋습니다. 코드 구조를 재설계하거나 의존성 주입 패턴을 사용하세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 CommonJS와 ES Modules 두 가지 모듈 시스템을 지원합니다. CommonJS는 require/module.exports를 사용하고, ES Modules는 import/export를 사용합니다. 새 프로젝트에서는 ES Modules 사용이 권장되며, 기존 프로젝트와의 호환성이 필요하면 CommonJS를 사용합니다. 모듈 캐싱과 순환 참조에 대한 이해도 중요합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/787</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C#entry787comment</comments>
      <pubDate>Sun, 22 Feb 2026 20:00:22 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 async/await 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-asyncawait-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. async/await란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async/await는 ES2017(ES8)에서 도입된 비동기 처리 문법으로, 프로미스를 기반으로 동작합니다. 비동기 코드를 마치 동기 코드처럼 작성할 수 있어 가독성이 크게 향상됩니다. async 함수는 항상 프로미스를 반환하며, await는 프로미스가 처리될 때까지 함수 실행을 일시 중지합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본 문법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 async 함수 선언&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 함수 선언식
async function fetchData() {
  return '데이터';
}

// 함수 표현식
const fetchData = async function() {
  return '데이터';
};

// 화살표 함수
const fetchData = async () =&amp;gt; {
  return '데이터';
};

// 메서드
const obj = {
  async getData() {
    return '데이터';
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 await 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getUser() {
  // await는 프로미스가 resolve될 때까지 대기
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

// async 함수는 프로미스를 반환
getUser().then(user =&amp;gt; console.log(user));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 콜백/프로미스와 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 콜백 방식&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function getUserData(userId, callback) {
  getUser(userId, (err, user) =&amp;gt; {
    if (err) return callback(err);
    getOrders(user.id, (err, orders) =&amp;gt; {
      if (err) return callback(err);
      getOrderDetails(orders[0].id, (err, details) =&amp;gt; {
        if (err) return callback(err);
        callback(null, { user, orders, details });
      });
    });
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 프로미스 체이닝&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function getUserData(userId) {
  let userData = {};
  return getUser(userId)
    .then(user =&amp;gt; {
      userData.user = user;
      return getOrders(user.id);
    })
    .then(orders =&amp;gt; {
      userData.orders = orders;
      return getOrderDetails(orders[0].id);
    })
    .then(details =&amp;gt; {
      userData.details = details;
      return userData;
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 async/await&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getUserData(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0].id);

  return { user, orders, details };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 에러 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 try/catch 사용&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;async function fetchUserSafely(userId) {
  try {
    const user = await getUser(userId);
    const orders = await getOrders(user.id);
    return { user, orders };
  } catch (error) {
    console.error('에러 발생:', error.message);
    throw error; // 필요시 에러 재전파
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 개별 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function fetchMultipleData() {
  // 각 작업별로 에러 처리
  const user = await getUser(1).catch(err =&amp;gt; {
    console.error('사용자 조회 실패');
    return null;
  });

  if (!user) return null;

  const orders = await getOrders(user.id).catch(err =&amp;gt; {
    console.error('주문 조회 실패');
    return [];
  });

  return { user, orders };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 에러 처리 유틸리티 함수&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 에러를 배열로 반환하는 유틸리티
async function to(promise) {
  try {
    const result = await promise;
    return [null, result];
  } catch (error) {
    return [error, null];
  }
}

// 사용
async function fetchData() {
  const [err, user] = await to(getUser(1));
  if (err) {
    console.error('에러:', err.message);
    return;
  }
  console.log('사용자:', user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 병렬 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 순차 실행 (느림)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function sequential() {
  // 각 작업이 끝나야 다음 작업 시작 - 총 3초
  const result1 = await delay(1000);
  const result2 = await delay(1000);
  const result3 = await delay(1000);

  return [result1, result2, result3];
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 병렬 실행 (빠름)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function parallel() {
  // 모든 작업 동시 시작 - 총 1초
  const [result1, result2, result3] = await Promise.all([
    delay(1000),
    delay(1000),
    delay(1000)
  ]);

  return [result1, result2, result3];
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 병렬 실행 with 개별 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function parallelWithErrorHandling() {
  const results = await Promise.allSettled([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);

  const users = results
    .filter(r =&amp;gt; r.status === 'fulfilled')
    .map(r =&amp;gt; r.value);

  const errors = results
    .filter(r =&amp;gt; r.status === 'rejected')
    .map(r =&amp;gt; r.reason);

  return { users, errors };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 반복문에서의 async/await&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 for...of (순차 실행)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function processSequentially(items) {
  const results = [];

  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }

  return results;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 map + Promise.all (병렬 실행)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function processParallel(items) {
  const results = await Promise.all(
    items.map(item =&amp;gt; processItem(item))
  );

  return results;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 forEach 주의사항&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 잘못된 사용 - forEach는 await를 기다리지 않음
async function wrong(items) {
  items.forEach(async (item) =&amp;gt; {
    await processItem(item); // 기다리지 않고 다음 반복 실행
  });
  console.log('완료'); // 모든 처리가 끝나기 전에 출력됨
}

// 올바른 사용
async function correct(items) {
  for (const item of items) {
    await processItem(item);
  }
  console.log('완료'); // 모든 처리가 끝난 후 출력
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실전 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 API 데이터 가져오기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function fetchAndSaveUser(userId) {
  try {
    // API 호출
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const user = await response.json();

    // 파일로 저장
    await fs.writeFile(
      `user_${userId}.json`,
      JSON.stringify(user, null, 2)
    );

    console.log(`사용자 ${userId} 저장 완료`);
    return user;
  } catch (error) {
    console.error(`사용자 ${userId} 처리 실패:`, error.message);
    throw error;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async/await는 프로미스 기반의 비동기 코드를 동기 코드처럼 작성할 수 있게 해주는 문법입니다. try/catch로 에러를 처리하고, Promise.all과 함께 사용하면 병렬 처리도 가능합니다. 반복문에서는 for...of를 사용하고, forEach는 async/await와 함께 사용하지 않는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/786</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-asyncawait-%EC%82%AC%EC%9A%A9%EB%B2%95#entry786comment</comments>
      <pubDate>Sun, 22 Feb 2026 14:00:07 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 프로미스(Promise) 사용법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%94%84%EB%A1%9C%EB%AF%B8%EC%8A%A4Promise-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로미스란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스(Promise)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. ES6(ECMAScript 2015)에서 도입되었으며, 콜백 지옥을 해결하고 비동기 코드를 더 읽기 쉽게 만들어줍니다. 프로미스는 pending(대기), fulfilled(이행), rejected(거부) 세 가지 상태를 가집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로미스의 기본 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 프로미스 생성&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const myPromise = new Promise((resolve, reject) =&amp;gt; {
  // 비동기 작업 수행
  const success = true;

  if (success) {
    resolve('작업 성공'); // 성공 시 resolve 호출
  } else {
    reject(new Error('작업 실패')); // 실패 시 reject 호출
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 프로미스 사용&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;myPromise
  .then(result =&amp;gt; {
    console.log('성공:', result);
  })
  .catch(error =&amp;gt; {
    console.error('실패:', error.message);
  })
  .finally(() =&amp;gt; {
    console.log('작업 완료 (성공/실패 무관)');
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로미스 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 비동기 함수를 프로미스로 감싸기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function delay(ms) {
  return new Promise(resolve =&amp;gt; {
    setTimeout(resolve, ms);
  });
}

// 사용
delay(2000).then(() =&amp;gt; {
  console.log('2초 후 실행');
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 HTTP 요청 예시&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const https = require('https');

function fetchData(url) {
  return new Promise((resolve, reject) =&amp;gt; {
    https.get(url, (res) =&amp;gt; {
      let data = '';

      res.on('data', chunk =&amp;gt; {
        data += chunk;
      });

      res.on('end', () =&amp;gt; {
        try {
          resolve(JSON.parse(data));
        } catch (err) {
          reject(err);
        }
      });
    }).on('error', reject);
  });
}

// 사용
fetchData('https://api.example.com/data')
  .then(data =&amp;gt; console.log(data))
  .catch(err =&amp;gt; console.error(err));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 파일 시스템 작업&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

// Node.js fs 모듈의 promises API 사용
async function readConfig() {
  const data = await fs.readFile('config.json', 'utf8');
  return JSON.parse(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 프로미스 체이닝&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 비동기 작업을 순차적으로 연결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function getUser(userId) {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; resolve({ id: userId, name: '홍길동' }), 100);
  });
}

function getOrders(userId) {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; resolve([{ id: 1, product: '노트북' }]), 100);
  });
}

function getOrderDetails(orderId) {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; resolve({ orderId, price: 1500000 }), 100);
  });
}

// 프로미스 체이닝
getUser(1)
  .then(user =&amp;gt; {
    console.log('사용자:', user.name);
    return getOrders(user.id);
  })
  .then(orders =&amp;gt; {
    console.log('주문 수:', orders.length);
    return getOrderDetails(orders[0].id);
  })
  .then(details =&amp;gt; {
    console.log('주문 금액:', details.price);
  })
  .catch(err =&amp;gt; {
    console.error('에러 발생:', err.message);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프로미스 정적 메서드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 Promise.all&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로미스가 완료될 때까지 기다립니다. 하나라도 실패하면 전체가 실패합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const promise1 = fetch('/api/users');
const promise2 = fetch('/api/products');
const promise3 = fetch('/api/orders');

Promise.all([promise1, promise2, promise3])
  .then(([users, products, orders]) =&amp;gt; {
    console.log('모든 데이터 로드 완료');
  })
  .catch(err =&amp;gt; {
    console.error('하나 이상 실패:', err);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 Promise.allSettled&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로미스의 결과를 기다립니다. 실패해도 다른 결과를 받을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;Promise.allSettled([promise1, promise2, promise3])
  .then(results =&amp;gt; {
    results.forEach((result, index) =&amp;gt; {
      if (result.status === 'fulfilled') {
        console.log(`${index}: 성공 -`, result.value);
      } else {
        console.log(`${index}: 실패 -`, result.reason);
      }
    });
  });&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 Promise.race&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 완료되는 프로미스의 결과를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const timeout = new Promise((_, reject) =&amp;gt; {
  setTimeout(() =&amp;gt; reject(new Error('타임아웃')), 5000);
});

const fetchData = fetch('/api/data');

Promise.race([fetchData, timeout])
  .then(data =&amp;gt; console.log('데이터:', data))
  .catch(err =&amp;gt; console.error('실패:', err.message));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 Promise.any&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 성공하는 프로미스의 결과를 반환합니다. 모두 실패해야 실패합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const servers = [
  fetch('https://server1.com/api'),
  fetch('https://server2.com/api'),
  fetch('https://server3.com/api')
];

Promise.any(servers)
  .then(response =&amp;gt; console.log('가장 빠른 응답:', response))
  .catch(err =&amp;gt; console.error('모든 서버 실패'));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 에러 처리&lt;/h2&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function riskyOperation() {
  return new Promise((resolve, reject) =&amp;gt; {
    const random = Math.random();
    if (random &amp;gt; 0.5) {
      resolve('성공');
    } else {
      reject(new Error('실패'));
    }
  });
}

// catch로 에러 처리
riskyOperation()
  .then(result =&amp;gt; console.log(result))
  .catch(err =&amp;gt; console.error('에러:', err.message))
  .finally(() =&amp;gt; console.log('작업 종료'));

// then의 두 번째 인자로 에러 처리
riskyOperation()
  .then(
    result =&amp;gt; console.log(result),
    err =&amp;gt; console.error('에러:', err.message)
  );&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스는 콜백의 단점을 보완한 비동기 처리 패턴으로, then/catch/finally를 통해 결과를 처리합니다. Promise.all, Promise.race 등의 정적 메서드로 여러 비동기 작업을 효율적으로 관리할 수 있습니다. 프로미스 체이닝을 통해 순차적인 비동기 작업을 가독성 있게 작성할 수 있으며, async/await와 함께 사용하면 더욱 직관적인 코드가 됩니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/785</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%ED%94%84%EB%A1%9C%EB%AF%B8%EC%8A%A4Promise-%EC%82%AC%EC%9A%A9%EB%B2%95#entry785comment</comments>
      <pubDate>Sun, 22 Feb 2026 11:00:52 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 콜백 함수(Callback Functions)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98Callback-Functions</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 콜백 함수란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수는 다른 함수에 인자로 전달되어 특정 시점에 호출되는 함수입니다. Node.js에서는 비동기 작업이 완료되었을 때 결과를 처리하기 위해 콜백 함수를 사용합니다. Node.js의 초기 비동기 처리 방식으로, 현재도 많은 내장 모듈에서 사용되고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 콜백 함수의 기본 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 동기 콜백&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 배열의 forEach는 동기 콜백
const numbers = [1, 2, 3, 4, 5];

numbers.forEach((num) =&amp;gt; {
  console.log(num);
});

console.log('완료');
// 출력: 1, 2, 3, 4, 5, 완료 (순차적)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 비동기 콜백&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

// fs.readFile은 비동기 콜백
fs.readFile('file.txt', 'utf8', (err, data) =&amp;gt; {
  if (err) {
    console.error('에러:', err.message);
    return;
  }
  console.log('파일 내용:', data);
});

console.log('파일 읽기 요청 완료');
// 출력: 파일 읽기 요청 완료 &amp;rarr; 파일 내용: ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Node.js 콜백 컨벤션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 콜백 함수는 Error-First Callback 패턴을 따릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 인자는 항상 에러 객체 (에러가 없으면 null)&lt;/li&gt;
&lt;li&gt;두 번째 인자부터 결과 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Error-First Callback 패턴
function readFileCallback(err, data) {
  // 첫 번째: 에러 체크
  if (err) {
    console.error('에러 발생:', err.message);
    return;
  }

  // 두 번째: 성공 시 데이터 처리
  console.log('데이터:', data);
}

fs.readFile('file.txt', 'utf8', readFileCallback);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 콜백 함수 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 커스텀 콜백 함수 만들기&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function fetchUserData(userId, callback) {
  // 비동기 작업 시뮬레이션
  setTimeout(() =&amp;gt; {
    if (!userId) {
      callback(new Error('userId가 필요합니다'), null);
      return;
    }

    const user = {
      id: userId,
      name: '홍길동',
      email: 'hong@example.com'
    };

    callback(null, user);
  }, 1000);
}

// 사용
fetchUserData(1, (err, user) =&amp;gt; {
  if (err) {
    console.error(err.message);
    return;
  }
  console.log('사용자 정보:', user);
});&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 여러 비동기 작업 연결&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.readFile('config.json', 'utf8', (err, configData) =&amp;gt; {
  if (err) {
    console.error('설정 파일 읽기 실패');
    return;
  }

  const config = JSON.parse(configData);

  fs.readFile(config.dataFile, 'utf8', (err, data) =&amp;gt; {
    if (err) {
      console.error('데이터 파일 읽기 실패');
      return;
    }

    console.log('데이터:', data);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 콜백 지옥(Callback Hell)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩된 콜백이 많아지면 코드 가독성이 떨어지는 콜백 지옥이 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 콜백 지옥 예시&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 콜백 지옥 - 가독성이 매우 떨어짐
getUser(userId, (err, user) =&amp;gt; {
  if (err) return handleError(err);

  getOrders(user.id, (err, orders) =&amp;gt; {
    if (err) return handleError(err);

    getOrderDetails(orders[0].id, (err, details) =&amp;gt; {
      if (err) return handleError(err);

      getProductInfo(details.productId, (err, product) =&amp;gt; {
        if (err) return handleError(err);

        console.log('최종 결과:', product);
      });
    });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 콜백 지옥 해결 방법&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 방법 1: 함수 분리
function handleUser(err, user) {
  if (err) return handleError(err);
  getOrders(user.id, handleOrders);
}

function handleOrders(err, orders) {
  if (err) return handleError(err);
  getOrderDetails(orders[0].id, handleDetails);
}

function handleDetails(err, details) {
  if (err) return handleError(err);
  console.log('결과:', details);
}

getUser(userId, handleUser);

// 방법 2: Promise 또는 async/await 사용 (권장)
async function getProductData(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0].id);
  return details;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 콜백을 프로미스로 변환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 util.promisify를 사용하면 콜백 기반 함수를 프로미스로 변환할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const util = require('util');
const fs = require('fs');

// 콜백 기반 함수를 프로미스로 변환
const readFileAsync = util.promisify(fs.readFile);

// 프로미스로 사용
readFileAsync('file.txt', 'utf8')
  .then(data =&amp;gt; console.log(data))
  .catch(err =&amp;gt; console.error(err));

// 또는 async/await로 사용
async function readFile() {
  const data = await readFileAsync('file.txt', 'utf8');
  console.log(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수는 Node.js 비동기 처리의 기본 패턴으로, Error-First Callback 컨벤션을 따릅니다. 중첩이 깊어지면 콜백 지옥이 발생할 수 있으므로, 함수 분리나 프로미스/async-await로의 전환을 고려해야 합니다. util.promisify를 활용하면 기존 콜백 기반 코드를 쉽게 프로미스로 변환할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/784</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98Callback-Functions#entry784comment</comments>
      <pubDate>Sun, 22 Feb 2026 08:00:36 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 비동기 프로그래밍(Asynchronous Programming)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8DAsynchronous-Programming</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비동기 프로그래밍이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 프로그래밍은 특정 작업이 완료될 때까지 기다리지 않고 다음 작업을 실행하는 프로그래밍 방식입니다. Node.js는 싱글 스레드 기반으로 동작하기 때문에 비동기 프로그래밍이 필수적입니다. 파일 읽기, 네트워크 요청, 데이터베이스 쿼리 등의 I/O 작업을 비동기로 처리하여 블로킹 없이 다른 요청을 처리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 동기 vs 비동기 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 동기 방식&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

console.log('1: 시작');

// 동기 방식 - 파일 읽기가 완료될 때까지 대기
const data = fs.readFileSync('file.txt', 'utf8');
console.log('2: 파일 내용:', data);

console.log('3: 완료');

// 출력 순서: 1 &amp;rarr; 2 &amp;rarr; 3 (순차적)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 비동기 방식&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const fs = require('fs');

console.log('1: 시작');

// 비동기 방식 - 파일 읽기를 기다리지 않고 다음 코드 실행
fs.readFile('file.txt', 'utf8', (err, data) =&amp;gt; {
  console.log('2: 파일 내용:', data);
});

console.log('3: 완료');

// 출력 순서: 1 &amp;rarr; 3 &amp;rarr; 2 (비동기)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Node.js의 비동기 처리 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 비동기 작업을 처리하는 세 가지 주요 방식이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;콜백(Callback)&lt;/b&gt;: 가장 기본적인 방식으로, 작업 완료 시 호출될 함수를 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로미스(Promise)&lt;/b&gt;: ES6에서 도입된 비동기 처리 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;async/await&lt;/b&gt;: ES2017에서 도입된 프로미스 기반의 동기적 코드 스타일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 1. 콜백 방식
fs.readFile('file.txt', 'utf8', (err, data) =&amp;gt; {
  if (err) throw err;
  console.log(data);
});

// 2. 프로미스 방식
const fsPromises = require('fs').promises;
fsPromises.readFile('file.txt', 'utf8')
  .then(data =&amp;gt; console.log(data))
  .catch(err =&amp;gt; console.error(err));

// 3. async/await 방식
async function readFile() {
  const data = await fsPromises.readFile('file.txt', 'utf8');
  console.log(data);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 비동기 패턴 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 병렬 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 비동기 작업을 동시에 실행하여 성능을 향상시킵니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function readFilesParallel() {
  const startTime = Date.now();

  // 병렬 실행 - 세 파일을 동시에 읽음
  const [file1, file2, file3] = await Promise.all([
    fs.readFile('file1.txt', 'utf8'),
    fs.readFile('file2.txt', 'utf8'),
    fs.readFile('file3.txt', 'utf8')
  ]);

  console.log(`소요 시간: ${Date.now() - startTime}ms`);
  return { file1, file2, file3 };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 순차 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 작업의 결과가 필요한 경우 순차적으로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function processSequential() {
  // 순차 실행 - 각 작업이 완료된 후 다음 작업 시작
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0].id);

  return details;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 에러 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 콜백의 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;fs.readFile('file.txt', 'utf8', (err, data) =&amp;gt; {
  if (err) {
    console.error('파일 읽기 실패:', err.message);
    return;
  }
  console.log(data);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 async/await의 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function readFileWithError() {
  try {
    const data = await fs.promises.readFile('file.txt', 'utf8');
    return data;
  } catch (err) {
    console.error('파일 읽기 실패:', err.message);
    throw err;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 비동기 프로그래밍은 싱글 스레드 환경에서 효율적인 I/O 처리를 가능하게 합니다. 콜백, 프로미스, async/await 세 가지 방식을 상황에 맞게 활용하고, Promise.all을 통한 병렬 처리로 성능을 최적화할 수 있습니다. 적절한 에러 처리를 통해 안정적인 비동기 코드를 작성하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/783</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8DAsynchronous-Programming#entry783comment</comments>
      <pubDate>Sat, 21 Feb 2026 23:40:16 +0900</pubDate>
    </item>
    <item>
      <title>Node.js의 이벤트 루프(Event Loop)</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84Event-Loop</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이벤트 루프란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는 Node.js가 단일 스레드임에도 불구하고 비동기 작업을 처리할 수 있게 해주는 핵심 메커니즘입니다. JavaScript 코드 실행, 콜백 처리, 네트워크 I/O, 타이머 등의 작업을 조율하며, Node.js의 논블로킹 I/O 모델의 근간이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는 libuv 라이브러리에 의해 구현되어 있으며, 운영체제의 커널 기능을 활용하여 효율적인 비동기 처리를 수행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 이벤트 루프의 단계(Phases)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프는 6개의 단계를 순환하며 실행됩니다. 각 단계는 실행할 콜백 큐를 가지고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;timers&lt;/b&gt;: setTimeout(), setInterval() 콜백 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pending callbacks&lt;/b&gt;: 이전 루프에서 지연된 I/O 콜백 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;idle, prepare&lt;/b&gt;: 내부적으로 사용되는 단계&lt;/li&gt;
&lt;li&gt;&lt;b&gt;poll&lt;/b&gt;: 새로운 I/O 이벤트 처리, I/O 관련 콜백 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;check&lt;/b&gt;: setImmediate() 콜백 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;close callbacks&lt;/b&gt;: socket.on('close') 같은 close 이벤트 콜백 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;   ┌───────────────────────────┐
┌─&amp;gt;│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │&amp;lt;─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 비동기 함수의 실행 순서&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 setTimeout vs setImmediate&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;setTimeout(() =&amp;gt; {
  console.log('setTimeout');
}, 0);

setImmediate(() =&amp;gt; {
  console.log('setImmediate');
});

// 실행 순서는 상황에 따라 다를 수 있음
// I/O 콜백 내부에서는 setImmediate가 항상 먼저 실행됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 콜백 내부에서 실행할 경우:&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.readFile('file.txt', () =&amp;gt; {
  setTimeout(() =&amp;gt; {
    console.log('setTimeout');
  }, 0);

  setImmediate(() =&amp;gt; {
    console.log('setImmediate');
  });
});

// 출력 순서: setImmediate &amp;rarr; setTimeout (항상 동일)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 process.nextTick과 Promise&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process.nextTick()과 Promise는 이벤트 루프의 단계와 별개로 마이크로태스크 큐에서 처리됩니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;console.log('1: 동기 코드');

setTimeout(() =&amp;gt; console.log('2: setTimeout'), 0);

Promise.resolve().then(() =&amp;gt; console.log('3: Promise'));

process.nextTick(() =&amp;gt; console.log('4: nextTick'));

console.log('5: 동기 코드');

// 출력 순서: 1 &amp;rarr; 5 &amp;rarr; 4 &amp;rarr; 3 &amp;rarr; 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 우선순위:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동기 코드 (Call Stack)&lt;/li&gt;
&lt;li&gt;process.nextTick() 큐&lt;/li&gt;
&lt;li&gt;Promise 마이크로태스크 큐&lt;/li&gt;
&lt;li&gt;이벤트 루프 단계별 큐 (timers, poll, check 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 이벤트 루프 블로킹 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 블로킹 코드 예시&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// 잘못된 예: 이벤트 루프를 블로킹하는 코드
function fibonacci(n) {
  if (n &amp;lt;= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 이 작업 동안 다른 모든 요청이 대기 상태가 됨
app.get('/slow', (req, res) =&amp;gt; {
  const result = fibonacci(45); // 수 초 소요
  res.json({ result });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 블로킹 방지 방법&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 방법 1: setImmediate로 작업 분할
function processLargeArray(array, callback) {
  const chunk = 1000;
  let index = 0;

  function processChunk() {
    const end = Math.min(index + chunk, array.length);
    for (; index &amp;lt; end; index++) {
      // 작업 수행
    }
    if (index &amp;lt; array.length) {
      setImmediate(processChunk);
    } else {
      callback();
    }
  }

  processChunk();
}

// 방법 2: Worker Threads 사용 (CPU 집약적 작업)
const { Worker } = require('worker_threads');

function runFibonacci(n) {
  return new Promise((resolve, reject) =&amp;gt; {
    const worker = new Worker('./fibonacci-worker.js', {
      workerData: n
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실전 활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 비동기 작업 순서 제어&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function orderedExecution() {
  console.log('1: 시작');

  await new Promise(resolve =&amp;gt; {
    setImmediate(() =&amp;gt; {
      console.log('2: setImmediate');
      resolve();
    });
  });

  await new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log('3: setTimeout');
      resolve();
    }, 100);
  });

  console.log('4: 완료');
}

orderedExecution();
// 출력: 1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 이벤트 루프는 단일 스레드 환경에서 비동기 작업을 효율적으로 처리하는 핵심 메커니즘입니다. timers, poll, check 등의 단계를 순환하며 콜백을 실행하고, process.nextTick과 Promise는 마이크로태스크로 우선 처리됩니다. CPU 집약적 작업으로 이벤트 루프를 블로킹하지 않도록 주의하고, 필요시 Worker Threads를 활용하는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/782</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84Event-Loop#entry782comment</comments>
      <pubDate>Sat, 21 Feb 2026 22:30:55 +0900</pubDate>
    </item>
    <item>
      <title>Node.js 설치 및 설정 방법</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Node.js 버전 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 공식 사이트에서 두 가지 버전을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;LTS (Long Term Support)&lt;/b&gt;: 안정성이 검증된 장기 지원 버전으로 프로덕션 환경에 권장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Current&lt;/b&gt;: 최신 기능이 포함된 버전으로 새로운 기능을 테스트할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 개발 및 운영 환경에서는 LTS 버전을 사용하는 것이 권장됩니다. 2024년 기준 LTS 버전은 20.x 시리즈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 운영체제별 설치 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Windows&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 공식 사이트(&lt;a href=&quot;https://nodejs.org)%EC%97%90%EC%84%9C&quot;&gt;https://nodejs.org)에서&lt;/a&gt; Windows Installer(.msi)를 다운로드하여 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 설치 후 버전 확인
node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 시 &quot;Add to PATH&quot; 옵션이 기본으로 선택되어 있어 별도의 환경 변수 설정이 필요 없습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 macOS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew를 사용한 설치가 가장 편리합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# Homebrew로 설치
brew install node

# 버전 확인
node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 공식 사이트에서 macOS Installer(.pkg)를 다운로드하여 설치할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 Linux (Ubuntu/Debian)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodeSource 저장소를 추가하여 최신 버전을 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# NodeSource 저장소 추가 (Node.js 20.x)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -

# Node.js 설치
sudo apt-get install -y nodejs

# 버전 확인
node -v
npm -v&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. NVM을 이용한 버전 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 프로젝트에서 서로 다른 Node.js 버전을 사용해야 할 때 NVM(Node Version Manager)이 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 NVM 설치&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# NVM 설치 (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# 터미널 재시작 후 확인
nvm --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 NVM 사용법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 설치 가능한 버전 목록 확인
nvm ls-remote

# 특정 버전 설치
nvm install 20.10.0

# LTS 버전 설치
nvm install --lts

# 설치된 버전 목록 확인
nvm ls

# 사용할 버전 선택
nvm use 20.10.0

# 기본 버전 설정
nvm alias default 20.10.0&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 프로젝트 초기 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 package.json 생성&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 프로젝트 폴더 생성 및 이동
mkdir my-project
cd my-project

# package.json 생성 (대화형)
npm init

# package.json 생성 (기본값으로 빠르게)
npm init -y&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 package.json 구조&lt;/h3&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;프로젝트 설명&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node index.js&quot;,
    &quot;dev&quot;: &quot;node --watch index.js&quot;
  },
  &quot;keywords&quot;: [],
  &quot;author&quot;: &quot;&quot;,
  &quot;license&quot;: &quot;ISC&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 패키지 설치&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 프로덕션 의존성 설치
npm install express

# 개발 의존성 설치
npm install --save-dev nodemon

# 전역 설치
npm install -g typescript&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 개발 환경 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 nodemon 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 변경 시 자동으로 서버를 재시작하는 nodemon을 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;npm install --save-dev nodemon&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json의 scripts에 추가:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;nodemon index.js&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 환경 변수 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotenv 패키지를 사용하여 환경 변수를 관리합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// .env 파일
PORT=3000
DATABASE_URL=mongodb://localhost:27017/mydb

// index.js
require('dotenv').config();
console.log(process.env.PORT); // 3000&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 설치는 공식 사이트에서 LTS 버전을 다운로드하거나, NVM을 사용하여 여러 버전을 관리하는 방법이 있습니다. 프로젝트 시작 시 npm init으로 package.json을 생성하고, nodemon과 dotenv를 설정하면 효율적인 개발 환경을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/781</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95#entry781comment</comments>
      <pubDate>Sat, 21 Feb 2026 19:59:11 +0900</pubDate>
    </item>
    <item>
      <title>Node.js란 무엇인가</title>
      <link>https://walking-and-walking.tistory.com/entry/Nodejs%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Node.js의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임입니다. 기존에 브라우저에서만 실행되던 JavaScript를 서버 사이드에서도 실행할 수 있게 해주는 환경으로, 2009년 Ryan Dahl에 의해 처음 개발되었습니다. Node.js를 사용하면 JavaScript 하나의 언어로 프론트엔드와 백엔드를 모두 개발할 수 있어 풀스택 개발의 진입 장벽을 낮춰줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Node.js의 핵심 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js가 다른 서버 사이드 기술과 차별화되는 핵심 특징들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 I/O (Non-blocking I/O)&lt;/b&gt;: 파일 읽기, 데이터베이스 조회 등의 작업을 기다리지 않고 다음 작업을 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 스레드 + 이벤트 루프&lt;/b&gt;: 하나의 스레드로 수천 개의 동시 연결을 효율적으로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NPM 생태계&lt;/b&gt;: 200만 개 이상의 패키지를 보유한 세계 최대의 오픈소스 라이브러리 생태계&lt;/li&gt;
&lt;li&gt;&lt;b&gt;크로스 플랫폼&lt;/b&gt;: Windows, macOS, Linux 등 다양한 운영체제에서 동일하게 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Node.js 설치 및 기본 사용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js를 시작하기 위한 기본적인 설치 방법과 사용 예제입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 설치 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 공식 사이트(nodejs.org)에서 LTS 버전을 다운로드하여 설치한 후, 터미널에서 버전을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node -v    # v20.x.x
npm -v     # 10.x.x&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 첫 번째 Node.js 프로그램&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;// hello.js
console.log('Hello, Node.js!');&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node hello.js
# 출력: Hello, Node.js!&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 간단한 웹 서버 만들기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const http = require('http');

const server = http.createServer((req, res) =&amp;gt; {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('안녕하세요, Node.js 서버입니다!');
});

server.listen(3000, () =&amp;gt; {
  console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 3000번 포트에서 웹 서버가 시작되며, 브라우저에서 접속하면 응답 메시지를 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6415716834128759&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 비동기 처리의 이해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 가장 중요한 개념인 비동기 처리 방식을 이해해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 동기 vs 비동기&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 동기 방식 - 순서대로 실행
const fs = require('fs');
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
console.log('파일 읽기 완료');

// 비동기 방식 - 파일 읽기를 기다리지 않음
fs.readFile('file.txt', 'utf8', (err, data) =&amp;gt; {
  console.log(data);
});
console.log('이 줄이 먼저 실행됩니다');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Promise와 async/await&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 Node.js에서는 콜백 대신 Promise와 async/await를 사용하여 비동기 코드를 더 깔끔하게 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs').promises;

async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log(data1, data2);
  } catch (error) {
    console.error('파일 읽기 실패:', error.message);
  }
}

readFiles();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Node.js의 활용 분야&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 다양한 분야에서 활용되고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;REST API 서버&lt;/b&gt;: Express.js, Fastify 등의 프레임워크를 활용한 백엔드 API 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 애플리케이션&lt;/b&gt;: Socket.io를 활용한 채팅, 게임, 협업 도구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로서비스&lt;/b&gt;: 가볍고 빠른 특성을 활용한 마이크로서비스 아키텍처&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CLI 도구&lt;/b&gt;: npm, webpack, eslint 등 개발 도구 제작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버리스&lt;/b&gt;: AWS Lambda, Vercel 등 서버리스 환경에서의 함수 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netflix, LinkedIn, Uber, PayPal 등 글로벌 기업들이 Node.js를 프로덕션 환경에서 사용하고 있으며, 특히 I/O가 많고 동시 접속자가 많은 서비스에서 뛰어난 성능을 발휘합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js는 JavaScript를 서버에서 실행할 수 있게 해주는 런타임으로, 비동기 I/O와 이벤트 기반 아키텍처를 통해 높은 동시성을 처리할 수 있습니다. 풍부한 NPM 생태계와 JavaScript 단일 언어 사용이라는 장점으로 웹 개발의 효율성을 크게 높여주며, 실시간 애플리케이션과 API 서버 개발에 특히 적합합니다.&lt;/p&gt;</description>
      <category>Node.js</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/780</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Nodejs%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80#entry780comment</comments>
      <pubDate>Sat, 21 Feb 2026 18:14:26 +0900</pubDate>
    </item>
    <item>
      <title>Flutter의 광고 통합</title>
      <link>https://walking-and-walking.tistory.com/entry/Flutter%EC%9D%98-%EA%B4%91%EA%B3%A0-%ED%86%B5%ED%95%A9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Flutter에서 광고 통합의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 앱 수익화를 위한 방법 중 가장 보편적인 수단은 광고입니다. 특히 광고 SDK를 앱에 통합하면, 사용자 기반을 활용하여 수익을 창출할 수 있으며 무료 앱 모델을 유지할 수 있는 기반이 됩니다. Flutter는 크로스 플랫폼 프레임워크로, Android와 iOS 앱을 동시에 개발할 수 있는 장점이 있지만, 광고 SDK 통합 시에는 각 플랫폼별 네이티브 요소와의 연결이 필요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 대표적인 광고 플랫폼과 Flutter 플러그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter에서 사용할 수 있는 대표적인 광고 플랫폼으로는 Google AdMob, Facebook Audience Network, Unity Ads, AppLovin 등이 있습니다. 그중 가장 널리 사용되는 플랫폼은 Google의 AdMob이며, Flutter용 공식 플러그인 &lt;code&gt;google_mobile_ads&lt;/code&gt;가 제공됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;google_mobile_ads:&lt;/b&gt; Google AdMob 광고를 Flutter에 통합할 수 있도록 도와주는 공식 패키지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;facebook_audience_network:&lt;/b&gt; Facebook 광고 네트워크를 위한 서드파티 플러그인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;unity_ads_plugin:&lt;/b&gt; Unity Ads를 Flutter에 연동하기 위한 플러그인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. AdMob 통합 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 &lt;code&gt;google_mobile_ads&lt;/code&gt; 패키지를 활용하여 Flutter 앱에 배너 광고를 통합하는 기본적인 예제입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 pubspec.yaml에 패키지 추가&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dependencies:
  google_mobile_ads: ^4.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 초기화 및 광고 로딩&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BannerAdExample(),
    );
  }
}

class BannerAdExample extends StatefulWidget {
  @override
  _BannerAdExampleState createState() =&amp;gt; _BannerAdExampleState();
}

class _BannerAdExampleState extends State {
  late BannerAd _bannerAd;
  bool _isAdLoaded = false;

  @override
  void initState() {
    super.initState();
    _bannerAd = BannerAd(
      adUnitId: 'ca-app-pub-xxxxxxxxxxxxx/xxxxxxxxxx', // 테스트용 ID로 대체 가능
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener(
        onAdLoaded: (_) {
          setState(() {
            _isAdLoaded = true;
          });
        },
        onAdFailedToLoad: (ad, error) {
          ad.dispose();
          print('Ad failed to load: $error');
        },
      ),
    )..load();
  }

  @override
  void dispose() {
    _bannerAd.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AdMob Banner Example')),
      body: Column(
        children: [
          if (_isAdLoaded)
            Container(
              height: _bannerAd.size.height.toDouble(),
              width: _bannerAd.size.width.toDouble(),
              child: AdWidget(ad: _bannerAd),
            ),
          Expanded(
            child: Center(child: Text('앱 본문 내용')),
          ),
        ],
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고 단위 ID는 실제 광고 수익을 받기 위해 각 플랫폼의 콘솔에서 발급받은 ID로 변경해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 광고 유형과 활용 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고는 여러 형태로 제공되며, 사용자 경험을 해치지 않으면서 효과적으로 수익을 창출하기 위한 전략이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;배너 광고:&lt;/b&gt; 앱 하단 또는 상단에 고정적으로 표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전면 광고 (Interstitial):&lt;/b&gt; 특정 이벤트 후 전체 화면으로 표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보상형 광고:&lt;/b&gt; 사용자가 광고를 시청하면 리워드를 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보상형 광고는 게임이나 콘텐츠 앱에서 특히 유용하며, 사용자의 자발적인 참여를 유도할 수 있습니다. 광고 빈도 조절과 UX 배려가 핵심입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flutter에서 광고 통합은 앱의 수익 모델을 강화하는 데 필수적인 요소입니다. Google AdMob과 같은 플랫폼은 공식 플러그인을 제공하여 안정적인 통합을 지원하며, 광고 유형에 따른 전략적 활용은 사용자 이탈을 줄이고 수익을 극대화할 수 있는 방법입니다. 앱의 성격에 맞는 광고 방식과 위치를 신중히 선택하여 통합하는 것이 중요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;</description>
      <category>Flutter</category>
      <author>wsstar</author>
      <guid isPermaLink="true">https://walking-and-walking.tistory.com/779</guid>
      <comments>https://walking-and-walking.tistory.com/entry/Flutter%EC%9D%98-%EA%B4%91%EA%B3%A0-%ED%86%B5%ED%95%A9#entry779comment</comments>
      <pubDate>Tue, 28 Oct 2025 10:00:30 +0900</pubDate>
    </item>
  </channel>
</rss>