반응형

require를 쓸까, import를 쓸까?
Node.js는 CommonJS(CJS)ECMAScript Modules(ESM) 두 방식을 모두 지원합니다. 이 글은 두 방식의 차이, 선택 기준, 상호 운용(interop), 마이그레이션 팁을 실무 중심으로 정리했습니다.


1) 빠른 요약: 언제 CJS, 언제 ESM?

  • CJS(CommonJS) — require, module.exports
    • 장점: 레거시/도구 호환 최고, 시작이 빠름
    • 언제: 기존 라이브러리/도구(CJS 중심)와의 호환이 최우선일 때
  • ESM(ECMAScript Modules) — import, export
    • 장점: 브라우저·번들러·TypeScript 생태계와 일관, 정적 분석/트리 셰이킹, Top-Level await
    • 언제: 신규 프로젝트 기본값으로 추천

실무 제안: 새 프로젝트는 ESM 통일 → 기존 CJS 패키지는 동적 import() 또는 createRequire로 연결.


2) 문법 & 실행 규칙 한눈에

A. CommonJS(CJS)

  • 파일 조건
    • 확장자 .cjs, 또는
    • package.json에 "type": "commonjs"(혹은 type 미지정)인 .js
  • 문법 예시
// module.cjs (또는 type: commonjs 환경의 module.js)
exports.abs = (n) => (n < 0 ? -n : n);
module.exports.util = () => 'ok';

// main.cjs
const mod = require('./module');    // .js 확장자 생략 가능
console.log(mod.abs(-3));
  • require 해석 규칙(요약)
    • require('./x') → ./x → ./x.js → ./x.json → ./x.node
    • 대상이 디렉터리면 package.json의 main/exports 우선 → 없으면 index.js(→.json→.node)
    • 확장자/디렉터리 자동 보정 O

B. ES Modules(ESM)

  • 파일 조건
    • 확장자 .mjs, 또는
    • package.json에 "type": "module"인 .js
  • 문법 예시
// module.mjs (또는 type: module 환경의 module.js)
export const abs = (n) => (n < 0 ? -n : n);

// main.mjs
import { abs } from './module.js';  // ESM은 확장자 명시가 원칙
console.log(abs(-3));
  • import 해석 규칙(요약)
    • 확장자/디렉터리 자동 보정 X → 경로에 확장자 필수
    • 패키지 import 시 package.json의 exports(서브패스 포함) 규칙을 엄격히 따름

3) package.json으로 프로젝트 기준 정하기

ESM 프로젝트(권장)

{
  "type": "module"
}

CJS 프로젝트(기본값과 동일)

{
  "type": "commonjs"
}

혼용 전략

  • 한 저장소에서 CJS와 ESM을 섞는 경우:
    • CJS 파일은 .cjs, ESM 파일은 .mjs 로 확장자 분리
    • 패키지 배포 시 exports에 조건부 내보내기(CJS/ESM 동시 제공) 구성

4) 상호 운용(Interop) — CJS ↔ ESM 함께 쓰기

CJS → ESM 불러오기 (동적 import)

// main.cjs
(async () => {
  const { abs } = await import('./module.mjs');
  console.log(abs(-3));
})();

 

ESM → CJS 불러오기 (default 형태로 옴)

// main.mjs
import legacy from './module.cjs';
const { abs } = legacy;

 

ESM에서 require 사용(특수 케이스)

// main.mjs
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjs = require('./legacy.cjs');
 

5) 자주 겪는 함정과 해결

exports vs module.exports (CJS)

  • exports.foo = ... (같은 객체를 가리킴)
  • exports = fn  (참조 교체 → 내보내기 끊김) → 객체 전체 교체는 module.exports = ...로

변수 이름 충돌

  • CJS의 내장 변수 module을 가리는 코드 X
// 나쁨
const module = require('./module');
// 권장
const myModule = require('./module');

경로/파일 상수 차이

  • CJS: __filename, __dirname 바로 사용 가능
  • ESM: import.meta.url 기반으로 계산
 
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);

 

 

확장자/디렉터리 보정

  • CJS(require): 자동 보정 O
  • ESM(import): 확장자 명시 필수, 디렉터리 import 금지

Top-Level await

  • ESM만 지원
// ESM에서만 가능
const data = await fetch(url).then(r => r.json());

 

6) 선택 가이드 & 마이그레이션 체크리스트

신규 프로젝트

  • ESM 통일("type": "module")
  • 브라우저/번들러/TS와 규칙 일치, 정적 분석/트리 셰이킹, Top-Level await

기존 CJS 유지

  • 도구/레거시 호환 최우선
  • ESM 패키지 연동: 동적 import(), createRequire 브리지 고려

점진 전환 체크리스트

  • package.json에 "type": "module" 추가, CJS 파일은 .cjs로 분리
  • 확장자 명시(ESM 규칙) 및 디렉터리 import 제거
  • __dirname/__filename → import.meta.url 기반 대체
  • 외부 패키지의 exports/서브패스 호환 확인
  • 빌드/테스트/번들러 설정(ESM 대응) 점검

7) 유틸 스니펫

CJS: 실제 로드 경로 확인

console.log(require.resolve('./module'));

ESM: JSON 가져오기(Node 20+)

import data from './data.json' with { type: 'json' };
 

ESM: CommonJS 패키지 가져오기 패턴

import cjsDefault from 'legacy-pkg';
import * as cjsAll from 'legacy-pkg';

8) 결론

  • Node.js는 CJS와 ESM을 모두 지원합니다.
  • 새 프로젝트는 ESM로 통일, 레거시/도구 호환이 중요하면 CJS 유지도 합리적입니다.
  • 혼용 시 .cjs/.mjs 분리, 동적 import(), **createRequire**로 안전하게 연결하세요.
 
728x90
반응형
반응형

Node.js 개발에서 TypeScript를 도입할 때 가장 먼저 마주치는 고민 중 하나는 바로 이것입니다.

TypeScript를 전역으로 설치할까? 로컬로 설치할까?

이 글에서는 두 방식의 차이점, 장단점, 실제 추천 시나리오까지 한번에 정리해드립니다.


✅ 전역 설치란?

전역 설치는 시스템 전체에 TypeScript를 설치하여 어느 프로젝트 디렉토리에서도 tsc 명령어를 사용할 수 있게 하는 방식입니다. 다음과 같은 명령어로 설치합니다:

npm install -g typescript

설치 후에는 터미널 어디에서나 다음 명령이 작동합니다:

tsc --version
tsc yourfile.ts

전역 설치의 장점

  • tsc 명령을 터미널 어디서든 바로 쓸 수 있다
  • CLI 툴 사용 시 빠르게 실행 가능
  • 전역 설정에 익숙한 개발자에게 편리

전역 설치의 단점

  • 버전 충돌 가능성: 프로젝트마다 TypeScript 버전이 다를 경우 문제 발생
  • 팀 협업에서 위험: 다른 개발자의 시스템에 같은 버전이 설치되어 있다고 보장할 수 없음
  • CI/CD 파이프라인에서 재현 불가

✅ 로컬 설치란?

로컬 설치는 해당 프로젝트 폴더 안에 TypeScript를 설치하는 방식입니다. 보통 개발 의존성으로 추가합니다:

npm install --save-dev typescript

설치 후에는 전역 명령 대신 npx를 사용해 실행합니다:

npx tsc
npx tsc --init

로컬 설치의 장점

  • 프로젝트별로 TypeScript 버전을 고정할 수 있어 안정성 확보
  • 모든 팀원이 동일한 환경에서 작업 가능
  • CI/CD 환경에서도 100% 재현 가능
  • package.json만 보면 어떤 버전이 쓰였는지 알 수 있다

로컬 설치의 단점

  • tsc를 직접 실행하려면 npx 또는 경로를 지정해야 한다
  • 여러 프로젝트를 오가며 전역 설치처럼 쓰고 싶으면 약간 번거로움

🔍 어떤 상황에 어떤 설치를 써야 할까?

  • 혼자 실습하거나 빠르게 테스트하는 경우
    전역 설치도 무방합니다.
  • 팀 프로젝트, 버전 관리, 협업, CI/CD 구성이 필요한 경우
    반드시 로컬 설치가 권장됩니다.

요즘 대부분의 실제 프로젝트는 typescript를 로컬로 설치하고 npx tsc 또는 tsc npm script로 실행합니다.


🧙 마무리

전역 설치는 빠르고 편리하지만, 협업과 재현성을 위해서는 로컬 설치가 훨씬 안전하고 추천되는 방식입니다.

최종적으로는 다음과 같은 전략을 추천합니다:

  • CLI 툴 실습이나 글로벌 테스트용 → 전역 설치
  • 실제 서비스, 팀 프로젝트, 오픈소스, CI/CD → 로컬 설치 (with --save-dev)

🎯 참고 명령어 요약

# 전역 설치
npm install -g typescript

# 로컬 설치
npm install --save-dev typescript

# 로컬 실행
npx tsc

 

728x90
반응형
반응형

기본적으로 new Date()는 UTC 기준으로 동작합니다. 이를 **KST(한국 표준시, UTC+9)**로 변환하는 방법을 설명합니다.

🚀 1. Intl.DateTimeFormat 사용 (권장)

JavaScript의 Intl.DateTimeFormat API를 사용하면 타임존 변환이 간단합니다.

const date = new Date();

const kstDate = new Intl.DateTimeFormat('ko-KR', {
  timeZone: 'Asia/Seoul',  // ✅ KST 적용
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  hour12: false, // ✅ 24시간제 사용
}).format(date);

console.log(`📌 KST 시간: ${kstDate}`);

 

💡 출력 예시:

📌 KST 시간: 2024. 02. 25. 15:30:45

Intl.DateTimeFormat을 사용하는 이유

  • 최신 표준 지원 (ECMAScript 국제화)
  • 시간대 자동 적용
  • 성능 최적화

 

🚀 2. toLocaleString() 사용

toLocaleString()을 사용하여 KST로 변환할 수도 있습니다.

const date = new Date();
const kstDate = date.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });

console.log(`📌 KST 시간: ${kstDate}`);

💡 출력 예시:

📌 KST 시간: 2024. 2. 25. 오후 3:30:45

✅ toLocaleString()은 간편하지만, 시간 형식이 다소 달라질 수 있음.

 

 

🚀 3. moment-timezone 사용 (강력한 기능 제공)

moment-timezone을 사용하면 더 다양한 포맷 지정 가능.

📌 설치

npm install moment-timezone

📌 코드 예제

const moment = require('moment-timezone');

const kstDate = moment().tz("Asia/Seoul").format("YYYY-MM-DD HH:mm:ss");

console.log(`📌 KST 시간: ${kstDate}`);

💡 출력 예시:

📌 KST 시간: 2024-02-25 15:30:45

moment-timezone 사용 시 장점

  • 여러 타임존 변환 가능
  • 포맷 지정 가능
  • Date 객체와 연동 가능

 

🚀 4. Date 객체에서 직접 변환 (UTC 오프셋 적용)

UTC 시간에 9시간을 추가하여 KST로 변환할 수도 있습니다.

const date = new Date();
const kstDate = new Date(date.getTime() + (9 * 60 * 60 * 1000)); // ✅ UTC+9 적용

console.log(`📌 KST 시간: ${kstDate.toISOString().replace('T', ' ').slice(0, 19)}`);

💡 출력 예시:

📌 KST 시간: 2024-02-25 15:30:45

✅ 직접 Date 객체를 변환하는 방법이지만, Intl.DateTimeFormat보다 추천하지 않음.

 

 

🎯 정리 (어떤 방법을 선택할까?)

방법코드 예제장점단점
Intl.DateTimeFormat ✅ new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul' }) 표준 지원, 성능 최적화 포맷 지정이 다소 제한적
toLocaleString() ✅ date.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) 간편 사용 출력 형식이 다를 수 있음
moment-timezone ✅ moment().tz("Asia/Seoul").format("YYYY-MM-DD HH:mm:ss") 강력한 포맷팅 기능 패키지 설치 필요
UTC 오프셋 변환 ✅ new Date(date.getTime() + (9 * 60 * 60 * 1000)) 직접 변환 가능 DST(일광 절약 시간) 고려 안됨

📌 추천 방법

  • 최신 프로젝트 & 표준 지원이 필요하면 → Intl.DateTimeFormat()
  • 간단하게 변환하려면 → toLocaleString()
  • 강력한 포맷팅이 필요하면 → moment-timezone
  • UTC에서 오프셋 변환하려면 → Date + 9시간 추가
728x90
반응형
반응형

fs.createReadStream + csv-parser 사용 시 비동기 처리에 주의하세요!


📌 문제 상황: 비동기 스트림에서 예상치 못한 동작

CSV 파일을 읽고, 특정 수량(quantity)만큼 데이터를 처리하려고 했지만, 모든 데이터가 처리됨. 😲

목표:

  • CSV 데이터에서 특정 개수(quantity)만 읽고 중단
  • 비동기 함수(await)가 정확히 작동하도록 수정

💥 문제 발생 코드: 비동기 흐름 제어 실패

import fs from 'fs';
import csv from 'csv-parser';

let count = 0;
const quantity = 20;
const winDataList: any[] = [];

fs.createReadStream(filePath)
  .pipe(csv())
  .on("data", async (data: CsvRow) => {
    if (count < quantity) {
      const email = data.email;
      const point = data.point;
      const choice = data.choice;
      const choiceImage = { a: "imagea", b: "imageb" }[choice]!;

      winDataList.push({ email, point, choice, choiceImage });
      console.log("Processing:", email, point);

      await updateEmailFile(email, fileName); // 비동기 작업
      await updateEmail(email);              // 비동기 작업

      count++;
    } else {
      return; // ❌ 스트림은 계속 실행됨
    }
  });

⚠️ 문제 원인:

  1. csv-parser는 비동기 흐름을 기다리지 않음.
  2. await 사용했지만, 스트림은 비동기 작업과 무관하게 계속 실행.
  3. 결과: 모든 CSV 데이터가 처리됨. 😱

해결 방법: 스트림 흐름을 명확히 제어

### 🎯 방법 1: stream.destroy()로 스트림 종료

count가 quantity에 도달하면 스트림 자체를 종료하여 추가 데이터를 읽지 않도록 합니다.

import fs from 'fs';
import csv from 'csv-parser';

let count = 0;
const quantity = 20;
const winDataList: any[] = [];

const stream = fs.createReadStream(filePath)
  .pipe(csv());

stream.on("data", async (data: CsvRow) => {
  if (count < quantity) {
    const email = data.email;
    const point = data.point;
    const choice = data.choice;
    const choiceImage = { a: "imagea", b: "imageb" }[choice]!;

    winDataList.push({ email, point, choice, choiceImage });
    console.log("Processing:", email, point);

    await updateEmailFile(email, fileName);
    await updateEmail(email);

    count++;

    if (count >= quantity) {
      stream.destroy(); // ✅ 스트림 완전 종료
    }
  }
});

stream.on("close", () => {
  console.log("Stream closed after processing", count, "items.");
});

stream.on("error", (err) => {
  console.error("Stream error:", err);
});

💡 장점:

  • 간단하고 빠른 구현
  • quantity에 도달하면 즉시 스트림 종료

단점:

  • 스트림 완전 종료: 이후 데이터는 절대 다시 읽을 수 없음

### 🎯 방법 2: pause() & resume()으로 정교한 제어

각 데이터 처리 시 스트림을 일시정지하고, 비동기 작업 완료 후 다시 재개합니다.

import fs from 'fs';
import csv from 'csv-parser';

let count = 0;
const quantity = 20;
const winDataList: any[] = [];

const stream = fs.createReadStream(filePath)
  .pipe(csv());

stream.on("data", async (data: CsvRow) => {
  if (count < quantity) {
    stream.pause(); // ✅ 데이터 흐름 일시 정지

    const email = data.email;
    const point = data.point;
    const choice = data.choice;
    const choiceImage = { a: "imagea", b: "imageb" }[choice]!;

    winDataList.push({ email, point, choice, choiceImage });
    console.log("Processing:", email, point);

    await updateEmailFile(email, fileName);
    await updateEmail(email);

    count++;

    if (count >= quantity) {
      stream.destroy(); // ✅ 최대 수량 도달 시 스트림 종료
    } else {
      stream.resume(); // ✅ 다음 데이터 읽기 재개
    }
  }
});

stream.on("close", () => {
  console.log("Stream closed after processing", count, "items.");
});

stream.on("error", (err) => {
  console.error("Stream error:", err);
});

💡 장점:

  • 비동기 흐름 제어 가능 (데이터 순서 보장)
  • 중간에 작업 재개 가능

단점:

  • 코드 복잡성 증가

🎯 방법 3: pipeline()으로 스트림 관리 최적화

Node.js 10+에서는 **stream.pipeline()**을 사용해 에러 핸들링비동기 흐름 관리를 더 효율적으로 처리할 수 있습니다.

import { createReadStream } from 'fs';
import { pipeline } from 'stream';
import csv from 'csv-parser';
import { promisify } from 'util';

const asyncPipeline = promisify(pipeline);

async function processCSV(filePath: string, quantity: number) {
  let count = 0;
  const winDataList: any[] = [];

  await asyncPipeline(
    createReadStream(filePath),
    csv(),
    async function* (source) {
      for await (const data of source) {
        if (count >= quantity) break;

        const email = data.email;
        const point = data.point;
        const choice = data.choice;
        const choiceImage = { a: "imagea", b: "imageb" }[choice]!;

        winDataList.push({ email, point, choice, choiceImage });
        console.log("Processing:", email, point);

        await updateEmailFile(email, fileName);
        await updateEmail(email);

        count++;
      }
    }
  );

  console.log("Processed", count, "records");
}

processCSV(filePath, 20).catch(console.error);

💡 장점:

  • 에러 핸들링 포함
  • 비동기 흐름 자연스럽게 제어 가능
  • 중간에 멈추거나 재시작 가능

단점:

  • Node.js 10+ 필요
  • 복잡한 로직에는 적합하지 않을 수 있음

결론: 어떤 방법을 선택할까?

방법장점단점추천 상황

stream.destroy() 빠르고 단순 이후 데이터 다시 읽기 불가 간단한 데이터 처리
pause() & resume() 비동기 흐름 제어, 순서 보장 코드 복잡성 증가 정교한 흐름 제어 필요 시
pipeline() 에러 핸들링, 비동기 흐름 최적화 Node.js 10+ 필요 대규모 스트림 작업

📊 💡 최종 추천

  • 빠르게 끝내야 한다면 → destroy()
  • 비동기 흐름을 제어하고 싶다면 → pause() & resume()
  • 에러 핸들링과 확장성을 고려한다면 → pipeline()

💬 마무리

Node.js의 스트림은 효율적인 데이터 처리를 가능하게 하지만, 비동기 흐름 관리에 유의해야 합니다. 특히 대용량 CSV 처리에서는 pause()와 destroy()를 적절히 사용하여 메모리 사용을 줄이고, 필요 시 pipeline()으로 에러 핸들링까지 고려하세요. 🚀

 

#NodeJS #JavaScript #CSVParsing #StreamAPI #AsyncProgramming #DataProcessing #BackendDevelopment #NodeJSTips #FullStackDevelopment #WebDevelopment #Asynchronous #CodingTips #DeveloperLife #CodeOptimization #ErrorHandling #NodeJSTutorial #BackendEngineer #ProgrammingTips #SoftwareEngineering #TechBlog

💡 활용 팁:

  • 기술 태그 (#NodeJS, #JavaScript, #StreamAPI) → 개발자 타겟팅
  • 문제 해결 태그 (#ErrorHandling, #CodeOptimization) → 검색 최적화
  • 커뮤니티 태그 (#DeveloperLife, #TechBlog) → 더 넓은 도달범위 확보
728x90
반응형
반응형

JavaScript를 포함한 대부분의 프로그래밍 언어는 IEEE 754 표준에 따른 이중 정밀도 부동소수점(floating point) 방식을 사용합니다. 이 방식은 실수를 이진수로 표현하는데, 모든 소수를 정확하게 표현할 수 없기 때문에 내부적으로 약간의 오차가 발생합니다.

예를 들어, 아래와 같이 0.2와 0.4를 더하면:

const result = 0.2 + 0.4;
console.log(result); // 0.6000000000000001

 

왜 정확히 0.6이 나오지 않을까요? 그 이유는 0.2와 0.4를 이진수로 표현할 때 무한소수로 나타나기 때문에 반올림 과정에서 미세한 오차가 누적되기 때문입니다.

부동소수점 오차의 원인

IEEE 754 표준에서는 모든 숫자를 2진법으로 표현합니다. 그러나 10진법의 소수 중에는 2진법으로 정확하게 표현할 수 없는 수들이 있습니다. 예를 들어,

  • 0.2는 2진수로 근사치로 표현되고,
  • 0.4 역시 근사치로 표현됩니다.

이러한 근사치 계산 과정에서 오차가 발생하고, 두 값을 더할 때 오차가 누적되어 예상치 못한 결과가 나오게 됩니다.

해결 방법

부동소수점 연산 오차 문제를 완화하기 위한 몇 가지 방법을 소개합니다.

1. 반올림 사용하기

연산 결과를 원하는 소수점 자리수로 반올림하는 방법입니다. Number.prototype.toFixed() 메서드를 사용하면 문자열로 반환되므로, 이를 Number() 함수를 통해 숫자형으로 변환할 수 있습니다.

const result = 0.2 + 0.4;
const rounded = Number(result.toFixed(2)); // 소수점 둘째 자리까지 반올림
console.log(rounded); // 0.6

이 방법은 결과 값을 표현할 때는 유용하지만, 연산 자체의 정확도를 높이는 방법은 아닙니다.

2. 정수 연산 후 나누기

실수를 직접 계산하는 대신 정수 연산을 한 후 최종 결과를 원하는 단위로 변환하는 방법입니다.

const result = (2 + 4) / 10;
console.log(result); // 0.6

이 방법은 소수점 이하의 연산을 정수로 처리하여 부동소수점 오차를 피할 수 있는 장점이 있습니다. 그러나 모든 경우에 적용하기는 어려울 수 있습니다.

3. 수학 라이브러리 사용하기

보다 정밀한 계산이 필요하거나 부동소수점 오차를 완전히 제어하고 싶다면, Decimal.jsBig.js와 같은 라이브러리를 사용하는 것이 좋습니다.

Big.js 예제:

const Big = require('big.js');

const result = Big(0.2).plus(Big(0.4));
console.log(result.toString()); // "0.6"

// 결과를 number 타입으로 변환하려면:
const numberResult = Number(result);
console.log(numberResult); // 0.6

이 방법은 라이브러리 내부에서 소수 연산의 오차를 보완하는 알고리즘을 사용하므로, 보다 신뢰할 수 있는 결과를 제공합니다.


결론

JavaScript의 부동소수점 연산은 IEEE 754 표준에 의해 발생하는 오차 때문에, 0.2 + 0.4의 결과가 0.6000000000000001로 나타납니다. 이러한 문제를 해결하기 위해서는:

  1. 반올림을 통해 원하는 자리수로 결과를 조정하거나,
  2. 정수 연산 후 나누기 방식을 사용하거나,
  3. Decimal.js, Big.js와 같은 수학 라이브러리를 활용하여 보다 정밀한 계산을 수행하는 방법이 있습니다.

상황에 따라 적절한 방법을 선택하여 부동소수점 연산 오차 문제를 완화할 수 있습니다.

 

 

728x90
반응형

+ Recent posts