npm 의존성 줄이기: 최신 Node.js 기능 15가지 정리

okorion·2025년 11월 14일

핵심: 예전엔 npm 패키지로만 가능하던 기능들이 이제는 Node.js 코어에 내장되어 있다.
결과: 의존성 줄고, 보안·유지보수·빌드 속도 모두 개선 가능.

이 글은 원문에서 정리한 “기존에 인기 있던 npm 패키지 ↔ 이를 대체하는 최신 Node 기능”을 하나씩 정리한 것이다.
정리 기준:

  • 기존에 쓰던 패키지
  • Node.js가 제공하는 대체 기능
  • 대략 도입/안정화 버전
  • 언제까지 패키지를 써야 하는지

0. 왜 이 변화가 중요해졌나?

Node 생태계는 오랫동안 아래 같은 패턴으로 굴러왔다.

  • “플랫폼이 부족한 부분 → npm 패키지로 메우기”
  • HTTP 유틸, 파일 시스템 헬퍼, 색상/로그 유틸, 테스트 러너 등등

하지만 최근 몇 년간:

  • 플랫폼 자체가 상당히 성숙
  • 브라우저/웹 표준 API(Node 내장)와의 정합성 강화
  • 보안·공급망 리스크(supply chain) 이슈 부각

→ 자연스럽게 “굳이 패키지를 쓸 필요가 없는 영역”이 계속 넓어지고 있다.

이제는 “무조건 npm부터 검색”이 아니라,

  1. Node 코어에서 이미 지원하는지 확인
  2. 굳이 외부 패키지를 쓸 이유가 있는지 검토

가 더 적절한 순서가 된다.


1. node-fetch → 글로벌 fetch()

예전

  • Node 환경에서 브라우저의 fetch()를 쓰려면 node-fetch 같은 패키지를 설치해야 했다.

지금

  • Node 18+ 에서는 글로벌 fetch() 제공.
  • 브라우저와 거의 동일한 API.
const res = await fetch('https://api.github.com/repos/nodejs/node');
const data = await res.json();
console.log(data.full_name); // "nodejs/node"
  • 도입:
    • v17.5.0: 실험적(Experimental)
    • v18.0.0: 안정화

언제까지 node-fetch를 써야 하나?

  • Node 18 미만 지원이 필요할 때 정도.
  • 그 외에는 기본 fetch()로 일원화하는 편이 이득.

2. ws(클라이언트) → 글로벌 WebSocket (클라이언트용)

예전

  • WebSocket 구현 시 ws 패키지가 사실상 표준.
    • 클라이언트/서버 모두.

지금

  • Node 21+ 에서 글로벌 WebSocket(클라이언트) 지원(실험적).
const ws = new WebSocket('wss://echo.websocket.org');

ws.onopen = () => ws.send('Hello!');
ws.onmessage = (event) => console.log('Received:', event.data);
  • 도입: v21.0.0 (클라이언트 WebSocket, Experimental)
  • 현재 기준: 아직 실험적, 완전한 stable로는 명시 X

언제 ws를 계속 써야 하나?

  • 서버 사이드 WebSocket 구현 시:
    • 여전히 ws 또는 그 위에 만들어진 고수준 라이브러리가 사실상 표준.
  • 안정성과 기능(백프레셔, 브로드캐스팅, 룸 등)이 필요할 때는 ws 유지.

3. Mocha/Jest/… → node:test

예전

  • 테스트 러너는 무조건 Mocha/Jest/Tap/Ava 같은 서드파티.

지금

  • Node는 node:test라는 내장 테스트 러너 제공.
import test from 'node:test';
import assert from 'node:assert';

test('addition works', () => {
  assert.strictEqual(2 + 2, 4);
});
  • 도입: v18.0.0 (실험적)
  • 안정화: v20.0.0 이후 stable

언제 서드파티 테스트 프레임워크를 계속 써야 하나?

  • 필요할 때:
    • 스냅샷 테스트
    • 강력한 mocking 라이브러리
    • watch 모드, 풍부한 플러그인 생태계
  • 결론:
    • 라이브러리/모듈 단위 테스트만 한다면 node:test만으로도 충분한 경우가 많다.
    • 큰 애플리케이션 전체 테스트에서는 여전히 Jest 등 프레임워크 유용.

4. sqlite3 / better-sqlite3node:sqlite (실험적)

예전

  • SQLite 사용 시:
    • sqlite3 (네이티브 바인딩, 컴파일 필요)
    • 또는 better-sqlite3(성능·동기 API 강점)

업그레이드/플랫폼 변경 시 네이티브 모듈 깨지는 이슈가 흔했다.

지금

  • Node가 실험적 node:sqlite 모듈을 도입.
import { open } from 'node:sqlite';

const db = await open(':memory:');
await db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
  • 아직은 Experimental 단계.

언제 커뮤니티 패키지가 여전히 필요할까?

  • 고성능 튜닝
  • 고급 기능(특정 SQLite 확장, 세밀한 옵션 등)
  • 안정적인 API가 필요할 때

현재로서는 “앞으로 이 방향으로 갈 것이다” 정도로 보고,
실무에서는 여전히 better-sqlite3 등이 주력이다.


5. chalk / kleurutil.styleText()

예전

  • CLI 로그 색상/스타일링:
    • chalk, kleur 등이 사실상 표준.

지금

  • Node의 node:util 모듈에 styleText() 내장.
import { styleText } from 'node:util';

console.log(styleText('red', 'Error!'));
console.log(styleText(['bold', 'green'], 'Success!'));
  • 도입: v20.12.0
  • 안정화: v22.17.0

언제 chalk를 쓸 이유가 남는가?

  • 복잡한 테마 시스템
  • 체이닝 문법(chalk.red.bold('msg')) 선호
  • 구버전 Node 지원

그 외에는 새 프로젝트에서는 기본 styleText부터 고려할 수 있다.


6. ansi-colors / strip-ansiutil.stripVTControlCharacters()

예전

  • ANSI 이스케이프 코드 제거:
    • strip-ansi, ansi-colors 등 사용.

지금

  • Node의 node:util에서 제공:
import { stripVTControlCharacters } from 'node:util';

const text = '\u001B[4mUnderlined\u001B[0m';
console.log(stripVTControlCharacters(text)); // "Underlined"
  • 장점:
    • 네이티브, 검증된 처리가 내장
  • 서드파티가 필요한 경우: 대부분 거의 없음.

7. globfs.glob()

예전

  • 파일 매칭 패턴(**/*.js 등)을 위해 glob 패키지 필수.

지금

  • Node 22+ 에서 fs.glob() 지원.
import fs from 'node:fs/promises';

const files = await fs.glob('**/*.js');
console.log(files);
  • 도입: v22.0.0 근방 (fs API 확장)
  • 안정화: v22.17.0 LTS 라인에서 안정적으로 사용 가능

언제 glob를 계속 써야 하나?

  • Node 22 미만을 타겟팅할 때
  • 또는 기존 코드베이스를 굳이 바꾸지 않을 때

8. rimraffs.rm({ recursive: true })

예전

  • 디렉터리 재귀 삭제:
    • 윈도우 호환성 문제 때문에 rimraf가 거의 필수.

지금

  • fs.rm()에서 재귀 삭제 지원:
import fs from 'node:fs/promises';

await fs.rm('dist', { recursive: true, force: true });
  • 도입:
    • fs.rm() + recursive 옵션은 Node 14+에서 안정적으로 사용 가능
  • 현재 LTS(v18, v20, v22)에서는 모두 기본 기능.

결론

  • 새 프로젝트에서 rimraf 설치할 이유는 거의 없다.
  • 구버전 Node 또는 기존 스크립트 유지 목적이 아니라면 fs.rm으로 통일.

9. mkdirpfs.mkdir({ recursive: true })

예전

  • 여러 단계 디렉터리 생성:
    • mkdirp 패키지를 많이 사용.

지금

import fs from 'node:fs/promises';

await fs.mkdir('logs/app', { recursive: true });
  • 도입: v10.12.0에서 recursive 옵션 추가
  • 이후 안정적으로 사용 가능.

결론

  • 마찬가지로 새 코드에서는 mkdirp를 쓸 이유가 거의 없다.
  • 단, 레거시/구버전 Node 지원 시만 예외.

10. uuid(v4) → crypto.randomUUID()

예전

  • UUID(v4) 생성: uuid 패키지 사용.

지금

import { randomUUID } from 'node:crypto';

console.log(randomUUID());
  • 도입: v14.17.0
  • 이후 안정적인 코어 기능.

언제까지 uuid를 쓰나?

  • 구버전 Node (14 미만) 호환성 유지
  • v1/v5 등 다른 버전의 UUID 필요 시

그 외에는 crypto.randomUUID()가 기본 선택지.


11. base64-js / atob polyfill → Buffer, atob, btoa

예전

  • 브라우저/Node 간 Base64 인코딩/디코딩 호환을 위해
    • base64-js
    • 자체 atob/btoa polyfill 등 사용.

지금

  • Node는 글로벌 atob/btoa + 기존 Buffer 제공.
const encoded = btoa('hello');
console.log(encoded); // "aGVsbG8="
console.log(atob(encoded)); // "hello"
  • 도입: atob/btoa 글로벌은 대략 Node 20 이후
  • 현재 LTS에는 내장.

언제 패키지가 필요한가?

  • 매우 오래된 Node 버전까지 감안해야 할 때 정도.

12. url-pattern → 글로벌 URLPattern (실험적)

예전

  • 라우트 매칭:
    • url-pattern 같은 패키지 사용.

지금

  • Node는 웹 표준 URLPattern API를 글로벌로 제공(실험적).
const pattern = new URLPattern({ pathname: '/users/:id' });
const match = pattern.exec('/users/42');
console.log(match.pathname.groups.id); // "42"
  • 도입: v20.0.0 (Experimental)
  • 현재: 아직 실험적

언제 url-pattern이 여전히 유효한가?

  • 안정적인 프로덕션 라우팅
  • Node 및 브라우저 간 일관된 동작이 중요할 때(실험 플래그 회피)

13. dotenv(기본 기능) → --env-file 플래그 (실험적)

예전

  • .env 파일 로딩:
    • dotenv 패키지가 사실상 표준.

지금

  • Node 실행 시 플래그로 .env 로딩:
node --env-file=.env app.js
  • 도입: v20.10.0 (Experimental)

언제 dotenv를 계속 써야 하나?

  • 변수 확장(${VAR}), 여러 .env 파일 병합 등 고급 기능 필요
  • 실험 기능을 피하고 싶은 프로덕션 환경
  • TS/빌드 툴/테스트 러너와의 정교한 통합

14. event-target-shim → 글로벌 EventTarget

예전

  • Node에는 EventEmitter는 있었지만,
    • 웹 표준 EventTarget이 필요할 경우 event-target-shim 사용.

지금

const target = new EventTarget();
target.addEventListener('ping', () => console.log('pong'));
target.dispatchEvent(new Event('ping'));
  • 도입: v15.0.0
  • 안정화: v15.4.0 이후 “no longer experimental”

의미

  • 브라우저/Node 양쪽에서 동일한 이벤트 모델 사용 가능
  • Web API 스타일을 서버 코드에도 자연스럽게 적용

15. tsc(기본 트랜스파일) → Node의 TypeScript 스트립 기능 (실험적)

예전

  • .ts 실행:
    • 항상 tsc 빌드 or ts-node 같은 런타임 트랜스파일러 필요.

지금

  • Node 21+ 에서 타입만 스트립해서 실행하는 실험 기능 제공:
node --experimental-strip-types app.ts
  • 도입: v21.0.0 라인 (Experimental)
  • 아직 안정화 전

언제까지 tsc가 필요할까?

  • 타입 체크(compile-time error) 필수
  • declaration 파일(.d.ts) 생성
  • 번들링/최적화/타겟 지정(ES5/ES2017 등)
  • CI 파이프라인과의 통합

즉, 이 기능은 “간단한 TS 스크립트를 빠르게 돌려볼 때” 정도로 보는 게 적절하고,
실제 프로덕션 빌드는 여전히 tsc 기반이라고 보면 된다.


16. 전체 흐름 요약: “npm 먼저 보지 말고, Node 코어부터 보자”

Node의 진화 방향은 명확하다.

  • 과거: “필요한 건 다 npm에서”
  • 현재:
    • 보편적인 기능은 점점 코어에 흡수
    • 플랫폼 자체의 DX 개선
    • 공급망 공격·보안 이슈에 대한 방어(의존성 최소화)

이 변화가 주는 실무적인 이점:

  1. 의존성 감소
    • 설치/빌드 시간 단축
    • 패키지 취약점 관리 비용 감소
  2. 공급망·보안 리스크 감소
    • 악성 패키지/하이재킹(hijack) 위험 줄어듦
    • 관리해야 할 라이브러리 버전·저자·권한 범위 축소
  3. 브라우저/서버 코드의 이식성 증가
    • fetch, URLPattern, EventTarget 등 웹 표준 API를 그대로 사용
    • 풀스택 코드에서 환경 차이 줄어듦

17. 실무 적용 체크리스트

실제 코드베이스에서 이 내용을 적용하려면:

  1. 의존성 스캔
    • package.json 기준으로 아래 패키지 사용 여부 체크:
      • node-fetch, ws, chalk, kleur, strip-ansi, glob, rimraf, mkdirp, uuid, dotenv, event-target-shim 등
  2. Node 버전 정책 확인
    • 서비스별 최소 Node 버전 정리
    • v18+/v20+/v22+를 어디까지 올릴 수 있는지 평가
  3. 교체 우선순위 선정
    • 위험도·효과 기준:
      • 보안/빌드 영향 큰 것부터:
        • rimraf → fs.rm
        • mkdirp → fs.mkdir
        • uuid → randomUUID
        • node-fetch → fetch
        • chalk/strip-ansi류 → util 스타일 함수들
  4. 부분 적용 → 회귀 테스트
    • 패키지 한 개씩 제거하면서:
      • 유닛/통합 테스트
      • 빌드 스크립트/CI 스크립트 확인
  5. 실험 기능 도입 여부 별도 판단
    • node:sqlite, URLPattern, --env-file, --experimental-strip-types 등은:
      • 실험용 브랜치나 내부 도구부터 시범 적용
      • 프로덕션 반영은 Node 릴리스/문서 추이를 보고 결정

profile
okorion's Tech Study Blog.

0개의 댓글