HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📖
공부한 책
/
Node.js 디자인 패턴
Node.js 디자인 패턴
/
Chap2. Node.js 필수 패턴

Chap2. Node.js 필수 패턴

리액터 패턴콜백 패턴캐치되지 않는 예외모듈resolving 알고리즘모듈 정의 패턴exports 지정하기 (named exports)함수 내보내기(Exporting a function)생성자 익스포트하기인스턴스 익스포트 하기관찰자 패턴

리액터 패턴

콜백 패턴

  • 연속 전달 방식(The Continuation-Passing Style)
  • 콜백은 함수의 맨 마지막 파라미터로 전달
  • 오류는 함수 바디의 제일 상단에서 처리 ( CPS 함수에 의해 생성된 오류는 항상 콜백의 첫번째 인수로 전달됨)

캐치되지 않는 예외

  • 비동기 콜백 내부에서 예외가 발생하게 되면 예외가 이벤트 루프로 이동하여 다음 콜백으로 전달되지 않음 → Node.js에서 이는 회복 불능의 상태, 어플리케이션 그냥 종료됨
  • 어플리케이션은 예외가 이벤트 루프에 도착하는 순간 중단됨
  • 그렇다 하더라도, 어플리케이션이 중단되기 전에 자원을 정리하거나 로그를 남길 수는 있음
  • 캐치되지 않은 예외는 어플리케이션의 일관성을 보장할 수 없는 상태로 만들기에 종료하는 것이 맞음

모듈

  • Javascript의 주요 문제점 중 하나가 네임스페이스가 없다는 것임
  • 전역 범위에서 실행되는 프로그램은 내부 어플리케이션과 종속된 라이브러리 코드의 데이터들로 인해 충돌이 발생할 수 있기에, 노출식 모듈 패턴이 있음 (= 그냥 공개될 부분만 exports 한다는 말임)
  • 위의 구현을 보면 module.exports가 어떻게 된 구조인지 알 수 있음
  • exports는 module.exports의 초기 값에 대한 참조일 뿐임(module.exports와 exports 의 차이점)
    • 따라서 exports 변수에 값을 할당하는 것은 아무 의미가 없음. 참조값만 바뀌는 것임(근본은 module.exports이기에)

resolving 알고리즘

  • 파일 모듈 : moduleName이 ‘/’ 로 시작하면 절대 경로라고 간주, ./으로 시작하면 상대경로로 간주
  • 코어 모듈 : moduleName이 / 또는 ./ 로 시작하지 않으면 알고리즘은 먼저 코어 Node.js 모듈 내에서 검색 시도
  • 패키지 모듈 : moduleName과 일치하는 코어 모듈이 없는 경우, 요청 모듈의 경로에서 시작하여 디렉터리 구조 탐색하여 올라가면서 node_modules 디렉토리 찾고 그 안에서 일치하는 모듈 찾기 계속함. 파일 시스템의 루트에 도달할 때까지 반복
일치하는 경우
  • <MODULENAME>.js
  • <ModuleName> / index.js
  • <moduleName>/package.json의 main 속성에 지정된 디렉터리 / 파일

모듈 정의 패턴

exports 지정하기 (named exports)

  • Node.js의 코어 모듈 대부분은 이 패턴을 사용함
  • CommonJs의 명세에는 public 멤버들을 공개하는 데 exports 변수만을 사용하도록 하고 있음. module.exports는 Node.js가 제공하는 모듈 정의 패턴의 광범위한 범위를 지원하기 위한 것

함수 내보내기(Exporting a function)

  • module.exports 변수 전체를 함수에 재할당하는 것
  • 장점
    • 모듈에 대한 명확한 진입점을 제공하는 단일 기능 제공 → 그것에 대한 이해와 사용을 단순화함
    • 최소한의 노출이라는 원리에 잘 맞아 떨어짐
  • 이 패턴의 응용은 익스포트된 함수를 다른 public API의 네임스페이스로 사용하는 것임
 

생성자 익스포트하기

  • 함수를 내보내는 모듈이 특화된 것
  • 차이점
    • 사용자에게 생성자를 사용하여 새 인스턴스를 만들 수 있게 함
    • 프로토타입을 확장하고 새로운 클래스를 만들 수 있는 기능도 제공할 수 있음
  • substack(함수 내보내기)패턴과 비교하여 훨씬 더 많은 모듈의 내부를 노출함. 다른 한편으로는 기능 확장에 있어 훨씬 더 강력할 수 있음

인스턴스 익스포트 하기

관찰자 패턴

fs.readFile('foo.txt', 'utf8', (err, data) => { if(err) handleError(err); else processData(data); });
const fs = require('fs'); function readJSONThrows(filename, callback) { fs.readFile(filename, 'utf8', (err, data) => { if(err) { return callback(err); } // 에러 없이 데이터만 전달 callback(null, JSON.parse(data)); }); };
이 경우는 JSON.parse에서 에러가 발생시, 이벤트 루프에서 예외가 발생하므로 에러를 잡지 못함. 잡을 방법이 없음
const fs = require('fs'); function readJSONThrows(filename, callback) { fs.readFile(filename, 'utf8', (err, data) => { let parsed; if(err) { return callback(err); } try { parsed = JSON.parse(data); } catch(err) { return callback(err); } // 에러가 없으면 데이터 전달 callback(null, parsed); }); };
위와 달리, 함수 호출 스택에서 에러가 발생한걸 잡아서 callback으로 넘기기에 에러 캐치가 가능함
process.on('uncaughtException', (err) => { console.error('This will catch at las the ' + 'JSON parsing exception : ' + err.message); process.exit(1); // 종료 코드 1(오류)로 어플리케이션을 종료. 없으면 어플리케이션 계속됨 });
"use strict"; const fs = require('fs'); //save the original require let originalRequire = require; function loadModule(filename, module, require) { const wrappedSrc = `(function(module, exports, require) { ${fs.readFileSync(filename, 'utf8')} })(module, module.exports, require);`; eval(wrappedSrc); } // We intentionally use var in the next line to avoid "SyntaxError: Identifier 'require' has already been declared" var require = (moduleName) => { console.log(`Require invoked for module: ${moduleName}`); const id = require.resolve(moduleName); //[1] if(require.cache[id]) { //[2] return require.cache[id].exports; } //module metadata const module = { //[3] exports: {}, id: id }; //Update the cache require.cache[id] = module; //[4] //load the module loadModule(id, module, require); //[5] //return exported variables return module.exports; //[6] }; require.cache = {}; require.resolve = (moduleName) => { //reuse the original resolving algorithm for simplicity return originalRequire.resolve(moduleName); }; //Load the entry point using our homemade 'require' require(process.argv[2]);
require() 함수의 대략적인 구현
// logger.js exports.info = (message) => { console.log('info : ' + message); }; exports.verbose = (message) => { console.log('verbose: ' + message); }; //main.js const logger = require('./logger'); logger.info('message'); logger.verbose('message2');
//logger.js 파일 module.exports = (message) => { console.log(`info : ${message}`); } module.exports.verbose = (message) => { console.log(`verbose: ${message}`); }; //main.js 파일 const logger = require('./logger'); logger('This is an informational message'); logger.verbose('This is a verbose message');
class Logger { constructor(name) { this.name = name; } log(message) { console.log(`[${this.name}] ${message}`); } info(message) { this.log(`info: ${message}`); } verbose(message) { this.log(`verbose: ${message}`); } } module.exports = Logger;
class Logger { constructor(name) { this.name = name; } log(message) { console.log(`[${this.name}] ${message}`); } info(message) { this.log(`info: ${message}`); } verbose(message) { this.log(`verbose: ${message}`); } } module.exports = new Logger('DEFAULT');