ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Node.js] Node.js에 대해 얕고 넓게 알아보자
    백엔드/Node.js 2020. 7. 14. 02:21

     

    '넓게'라고 써놓기는 했으나 기본적으로 알아야 되는 내용들만 포스팅할 예정입니다.

    혹시 내용 중에 틀린 부분을 발견하셨다면 댓글 부탁드립니다!


     

    출처: https://github.com/nodejs/node

    0. Node.js의 탄생 배경

    2008년 9월 구글은 Chrome 웹 브라우저의 베타 버전을 발표했다. 여기에 탑재된 V8 자바스크립트 엔진은 이전에 개발된 다른 자바스크립트 엔진보다 빠른 속도를 보장했다. 이러한 Chrome 웹 브라우저의 코드를 오픈 소스화 하면서 V8 자바스크립트 엔진 코드 역시 공개되었고, 이 덕분에 속도가 느렸던 기존의 자바스크립트 엔진들을 V8 자바스크립트 엔진이 대체하여 Javascript 언어 속도가 향상되었다. 이렇게 속도가 빨라진 javascript의 사용 범위는 넓어졌고, 2009년 개발자 라이언 달(Ryan Dahl)에 의해 웹 서버 제작을 목적으로 개발된 Node.js가 최초 공개되었다. Node.js는 The Birth of Node: Where Did it Come From? Creator Ryan Dahl Shares the History에 나와있듯이 동시에 두 가지 일을 처리해야 될 필요성이 대두됨으로써 탄생했으며, 이 점이 Node.js의 특성이자 장점으로 이어졌다.

     “Node was originally born out of this problem — how can you handle two things at the same time?  Non-blocking sockets is one way. Node is more or less the idea, or exploring the idea: what if everything was non-blocking? What if you never waited for any IO to happen?”

     

     

    1. Node.js 란 무엇인가

    Javascript는 원래 웹 브라우저에 종속적인 언어였으나, Node.js의 탄생으로 인해 서버 사이드 개발도 가능하게 되었다. 공식 문서에 따르면  Node.js는 비동기 이벤트 주도(이벤트 발생 시 미리 지정한 작업을 수행하는 방식) JavaScript 런타임(Javascript 프로그램이 동작하는 환경)으로써, 확장성 있는 네트워크 애플리케이션을 만들 수 있도록 설계된 소프트웨어 플랫폼이다. HTTP 서버 라이브러리가 기본적으로 내장되어 있기 때문에 Apache, Nginx 같은 웹 서버가 없어도 서버를 구축할 수 있다 (Node.js 만으로 서버를 구축하면 위험하다는 얘기도 있지만 '얕고 넓게'라는 방향성에 맞춰 이번 포스팅에선 다루지 않겠다). 그렇다면 우리가 Node.js를 사용함으로써 얻는 장단점은 무엇일까?

    장점

    • 자바스크립트만으로 프론트엔드와 백엔드 모두 개발할 수 있게 되었다
    • 구글이 개발한 V8 엔진 기반이기 때문에 (구글이 망하지 않는 이상) 성능은 계속 향상될 것이다
    • 싱글 스레드(single thread) 기반의 비동기 I/O 처리 덕분에 높은 처리 성능을 제공한다
    • 이벤트 기반 비동기 방식이기 때문에 서버 리소스를 비교적 적게 사용한다
    • npm을 통해 다양한 모듈을 설치해서 사용할 수 있다

    단점

    • 이벤트 기반 비동기 방식이기 때문에 로직이 복잡하면 callback hell이 발생할 수 있다
    • 싱글 스레드인 관계로 단일 처리가 큰 경우 전체 시스템의 성능이 급격하게 떨어진다
    • 스크립트 언어 특성상 코드가 수행되어야 에러 유무가 판단되고, 에러가 발생하면 프로세스가 죽기 때문에 테스트, 디버깅, 장애 대응에 신경을 많이 써야 된다.

     

    위 장단점을 종합해보면 Node.js를 도입하기 좋은 서비스와 그렇지 않은 서비스를 알 수 있다. 동시에 수많은 요청을 처리해야 되며 빠른 응답 시간을 보여줘야 될 경우, 네트워크 스트리밍 서비스 및 채팅 서비스와 같이 비동기 방식이 어울리는 서비스의 경우 Node.js로 개발하기 적합하다. 반면 단일 처리가 오래 걸리고 로직이 복잡한 경우는 Node.js로 개발하기에 부적합하다.

     

     

    2.  Single thread 기반의 비동기 처리

    싱글 스레드(Single thread)는 하나의 프로세스에서 하나의 스레드가 실행되며, 멀티 스레드(Multi thread) 방식은 프로세스를 일정 단위로 나누고 여러 개의 스레드들이 실행된다.

     

    출처: https://m.blog.naver.com/three_letter/220333796848

    멀티 스레드 방식의 경우 동기화에 신경을 써야 하며, 각 스레드마다 리소스가 필요하기 때문에 서버에 부담이 갈 수 있다. Node.js의 핵심 키워드인 싱글 스레드 기반 비동기 처리는 이러한 문제를 해결해주었다. 스레드가 하나밖에 없는데 어떻게 다수의 클라이언트로부터 오는 요청을 어떻게 효율적으로 처리할 수 있을까? 이 문제를 바로 비동기적 처리가 이를 해결해준다. 만약 어떤 클라이언트로부터 I/O 작업을 요청받은 경우 해당 작업을 I/O 컨트롤러에 전달하고 돌아와 CPU 일을 하다가 I/O 연산이 종료되면 이를 클라이언트에게 전달하고 다시 CUP 일을 한다. 즉 어떤 요청이 들어왔다고 해서 그 요청 처리가 끝날 때까지 대기하는 것이 아니라, 해당 요청이 종료되기 전까지 계속하던 일을 하는 것이다. 일상생활에 빗대어 설명하자면 친구가 30분 뒤에 약속 장소에 도착한다고 했을 때, 친구가 오기 전까지 편의점도 들리고 사진도 찍고 sns도 하는 게 비동기적 방식이며, 친구가 올 때까지 그 자리에서 아무것도 하지 않으며 기다리기만 하는 게 동기적 방식이라 할 수 있겠다.

    출처: https://bcho.tistory.com/881

     

     

    3. Event loop

    Node.js의 Event loop에 대해 알아보기 전, Node.js의 아키텍처를 그림으로 간단히 살펴보자.

    출처: https://blog.usejournal.com/nodejs-architecture-concurrency-model-f71da5f53d1d

    Node.js는 C++와 Javascript로 개발된 플랫폼이며 위에서 언급했듯이 V8 엔진으로 구현되었다. V8 엔진은 우리가 입력한 언어를 컴퓨터가 알아들을 수 있는 언어로 컴파일해주고 최적화 작업을 통해 속도 및 성능을 향상해주는 역할을 한다. 그림상 V8 엔진 옆에 있는 libuv는 비동기 I/O 작업에 초점을 맞춘 플랫폼 지원 라이브러리로, 오픈 소스로 제공되고 있다. https://github.com/libuv/libuv 

     

    libuv/libuv

    Cross-platform asynchronous I/O. Contribute to libuv/libuv development by creating an account on GitHub.

    github.com

    공식 문서에 설명되어있듯이 libuv이 제공하는 다양한 기능들에는 event loop과 thread pool 기능도 포함되어 있다. 이 기능들이 Node.js의 중요한 특성인 Non-blocking single thread를 가능하게끔 해주는 역할을 한다.

    Node 프로세스의 single thread가 내부적으로 어떻게 동작하는지 간단하게 알아보자. 먼저 프로그램이 초기화되고 프로그램에 필요한 모듈들을 require 해온다. 그다음 event callback을 등록한다. 여기까지 진행되면 event loop가 시작된다. 즉 event loop에서는  callback 함수들만 실행되는 것이다. 그럼 모든 callback 함수들이 event loop에서 실행되느냐? 그건 아니다. 무거운 작업(File system API, crypto, compression, DNS lookup 등)을 single thread에서 수행하면 blocking 될 수 있으므로 이런 경우엔 해당 작업을 thread pool로 넘겨준다(작업이 무거운지 아닌지에 대한 판단 및 thread pool로 넘겨주는 일은 모두 libuv가 담당하므로 개발자는 이에 대해 신경 쓰지 않아도 된다). 이제 event loop에 event의 callback 함수가 정상적으로 넘어왔다고 생각해보자. thread 내부 작업에 단계가 있듯이, event loop도 여러 단계(phase)로 이루어져있다. 들어오는 이벤트는 그 특성에 맞는 phase에서 처리를 진행한다. 이 포스팅에선 간단히 알아보기로 했으므로 중요한 4단계만 정리해보겠다. 각 단계에는 받아온 event의 callback 함수들이 저장되어 있는 callback queue가 존재한다. callback queue에 저장된 함수를 call하고, 작업이 종료되지 않으면 다음 단계로 넘어가 다음 단계에 있는 callback queue의 함수를 call 한다. callback queue에 작업이 남아있을 때까지 이 과정을 반복하는 게 event loop의 큰 흐름이다. event loop의 첫 번째 단계는 Expired timer callbacks이다. 이름에서 느껴지듯 setTimeOut과 같은 timer 관련 작업들이 실행되는 단계이다. 두 번째 단계는 I/O polling and callbacks이다.(대부분의 이벤트는 poll에 들어온다고 한다) 네트워킹이나 파일 액세스 작업들이 여기서 수행된다. 세 번째 단계는 setImmediate callbacks이다. 마지막 단계는 close callbacks로, 웹 서버 또는 웹 소켓 shutdown 작업이 수행되는 단계이다. 마지막 단계까지 진행된 뒤, 남아있는 timer 또는 I/O 작업이 있는지 확인한 다음 남아있는 작업(pending)이 없다면 Node application을 종료한다. event loop는 Node 프로세스 시작과 동시에 동작하기 때문에 이런 측면에서 event loop는 Node 프로세스가 실행되는 내내 동작한다는 생각을 했다 

     

    4. 모듈(module)

    모듈은 파일 단위로 작성되며 키워드를 통해 모듈화 할 파일을 정하고, 다른 파일에서 이를 불러와 마치 라이브러리처럼 사용한다. 

    // people.js
    const property = function(name, age, gender){
      return `${name}는 ${age}세 ${gender}입니다. `
    }
    
    const physical = function(height, weight){
      let measure = weight / Math.pow((height * 0.01), 2);
      return measure >= 23 ? '비만입니다.' : '비만이 아닙니다.';
    }
    
    exports.property = property; // 내보내기
    exports.physical = physical; // 내보내기
    // member.js
    const people = require("./people.js"); // 불러오기
    
    let result = '';
    result = people.property('김코딩', 20, '여자');
    result += people.physical(160, 50);
    
    console.log(result); // 김코딩는 20세 여자입니다. 비만이 아닙니다.

    코드를 통해 확인할 수 있듯이 people.js 파일에서 내보낼 함수명을 exports 객체에 value값으로 할당하고, member.js 파일에서 require 키워드로 모듈을 로드한다. exports의 key값으로는 아무거나 올 수 있지만 가독성을 위해 웬만하면 함수명과 동일하게 지정하는 게 좋다.

    // 모듈화할 때
    const func1 = function(){ ... };
    const func2 = function(){ ... };
    
    exports.["func1의 key 값"] = func1;
    exports.["func2의 key 값"] = func2;
    
    
    // 모듈화 된 것을 사용할 때
    // require()은 module.exports를 반환한다
    const loadModule = require('모듈화된 파일의 경로');
    
    loadModule.["func1의 key 값"]; // func1 함수 실행
    loadModule.["func2의 key 값"]; // func2 함수 실행

    주석에도 나와있지만, 개발을 진행하다 보면 exports 외에 module.exports, export 키워드도 보게 된다. 각 키워드는 무슨 차이가 있을까?

     

    exports와 module.exports의 관계

    exports는 modules.exports의 shortcut(단축키)이다. 이게 끝이다. 즉, (func1라는 함수가 이미 선언되어있는 상황인 경우라면) module.exports.func1 = func1는 exports.func1 = func1과 동일한 의미이다. 똑같은 기능을 왜 굳이 이렇게 두 가지 키워드로 나눈 것일까? 그야 module.exports 보다 exports 쓰는 게 덜 귀찮기 때문이다. 그럼 앞으로 모듈화 할 때 exports만 쓰면 되는 것일까? 아니다. 만약 새로운 값을 exports에 바로 할당한다면, 이는 단지 로컬 변수 exports에 값을 할당된 것이고 exports는 module.exports에 바인딩될 수 없다. 코드를 보면 좀 더 이해하기 쉽다. 

    module.exports = function func1(){ ... }; // (O)
    module.exports.func2 = true; // (O)
    exports = function func1(){ ... }; // (X)
    exports = { func2: true }; // (x)
    
    const func3 = function(){ ... };
    module.exports.func3 = func3; // (O)
    exports.func3 = func3; // (O)

    간단히 얘기하자면 객체(javascript에선 모든 것이 객체이기 때문에) 선언과 동시에 바로 내보내길 원한다면 module.exports를 사용하면 되고, 먼저 선언을 먼저 하고 이후에 내보내길 원한다면 exports(물론 module.exports도 사용 가능)을 이용하면 된다. 하나의 파일에서 몇 개의 객체만 모듈화 한다면 exports를 사용해도 괜찮겠지만 만약 몇십 개의 객체를 모듈화 해야 된다면 일일이 exports로 내보내는 게 더 귀찮을 것이다. 이럴 때 내보내는 객체들을 하나의 객체로 묶은 다음 module.exports를 사용하면 훨씬 깔끔한 코드가 될 것이다. 일단 내가 알아본 것은 여기까지인데 새롭게 알게 된 내용이 있으면 추가하겠다.

     

    exports와 export의 관계

    exports는 CommonJS의 키워드이며 export는 Javascript ES6 문법에서 사용되는 키워드이다. ES6 문법에서 제공하는 모듈 내보내기/불러오기 방법을 살펴보자. 우선 모듈을 내보낼 때는 export, 불러올 때는 import를 이용하며, export는 named export, default export로 구분할 수 있다. 

    // Named export
    const func1 = function(){ ... };
    const func2 = function(){ ... };
    export { func1, func2 };
    export const func3 = function(){ ... };
    export function func4(){ ... };
    export let var1 = 'hi!';
    
    // as를 통해 이름변경도 가능하기에, 식별자 충돌을 피할 수 있다
    export { name1 as newName1, name2 as newName2 ... };
    
    
    // Default export
    const existFunc = function(){ ... };
    export { existFunc as default };
    export default function(){ ... };
    export default class{ ... };

    named export와 default export의 차이점을 정리해보자면 아래와 같다.

    • 하나의 파일에 named export는 여러 개 나와도 상관없지만 default export는 하나만 존재해야 된다.
    • named export에서는 let, const, var 사용이 가능하지만 default export에서는 사용할 수 없다.
    • named export로 내보낸 경우 동일한 이름으로 불러와(import) 사용해야 되지만, default export로 내보낸 경우 어떤 이름으로 불러와도 상관없다.

     

    내보내는 방법을 봤으니 불러올 때의 경우도 살펴보자.

    // named로 export된 모듈을 불러올 때
    import name1 from './namedFunc.js';
    
    // 구조분할을 통해 갖고 오고 싶은 모듈만 불러올 수 있다
    import { name1, name2 } from './namedFunc.js';
    
    // as를 통해 모듈에 별명을 붙여 가독성을 높일 수 있다
    import * as myModule from './namedFunc.js';
    
    
    // default로 export된 모듈을 불러올 때
    import dfModule from './defaultFunc1.js';
    
    // default, named로 export된 모듈을 한 번에 불러올 수도 있다
    import dfModule, {named1, name2} from './modulesFunc.js';

    모듈 사용법에 대해 아주 간단히 정리해보았다. 아직까지 ES6 문법이 도입되지 않은 서비스도 있으니 두 가지 경우 모두 알아야 되며 두 개의 경우를 혼용하면 안 된다.

     

     

    5.  npm과 nvm

    npm (node package manager)

    npm은 말 그대로 node의 패키지를 관리해주는 기능을 한다(npm은 node.js와 같이 설치되므로 따로 설치할 필요 없음). 해당 사이트에는 다양한 패키지들이 존재하며, 이를 통해 node.js 기반의 다양한 라이브러리 및 모듈을 설치하여 사용할 수 있다. npm으로 설치한 모듈들은 package.json 파일에 기록된다. package.json은 프로젝트에 대한 명세로서 프로젝트 이름, 설명, 버전과 같은 프로젝트 정보와, 설치된 패키지 이름 및 버전과 같은 의존성 등이 정의되어있다. (--save 옵션을 통해 설치된 Node 모듈은 package.json 파일 내의 dependencies 목록에 추가된다. 이후 app 디렉터리에서 npm install을 실행하면 종속 항목 목록 내의 모듈이 자동으로 설치된다)

    {
      "name": "<프로젝트 이름>",
      "version": "<프로젝트 버전>",
      "description": "<프로젝트 설명>",
      "main": "<프로젝트(프로그램)의 entry point>",
      "scripts": { // 프로젝트(프로그램)에서 실행하는 명령
        "<key>" : "<value>"
        // ex) "start": "nodemon --exec babel-node init.js --delay 2"
      },
      "author": "<작성자 이름>",
      "license": "<사용중인 license>",
      "dependencies": { // production 환경에서 필요한 패키지
        "<패키지 이름>": "<패키지 버전>"
      },
      "devDependencies": { // dev, test 환경에서 필요한 패키지
      	"<패키지 이름>": "<패키지 버전>"
        ...
      }
    }

    이 외에 더 많은 속성을 알고 싶다면 공식 문서를 참고하면 된다.

     

    nvm (node version manager)

    nvm은 노드 버전 관리자로서, 하나의 시스템(그냥 우리 컴퓨터라고 생각하면 쉽다)에 다양한 노드 버전을 설치할 수 있게 하고, 각 버전이 충돌하지 않도록 관리해준다.

     

     

    6. Expressjs

    Express는 웹과 모바일 애플리케이션을 위한 강력한 기능을 제공하는 Node.js의 작고 유연한 웹 애플리케이션 프레임워크다. HTTP 유틸리티 메서드 및 미들웨어를 통해 쉽고 빠르게 강력한 API를 작성할 수 있으며 Express를 기반으로 한 많은 프레임워크들이 존재한다. HTTP 요청 바디 파싱, 쿠키 파싱, 세션 관리, 라우팅 등 다양한 작업들을 간단히 처리할 수 있으며, MVC 구조를 제공한다. 한 마디로 개발자들이 Node.js를 갖고 좀 더 쉽게 개발할 수 있게끔 도와주는 기능을 한다. 설치 및 API 설명은 공식 문서를 참고하면 된다.

     

     

     

     

    마무리

    글 작성이 늘어져서 일단 여기서 마무리를 해야겠다. 기본적인 내용들인데도 정리하다 보니 공부할게 너무 많아 이 포스팅에 내용을 다 쓰지 못했다. Node.js를 접한 지 반년도 안 된 입장에서 깊게 공부하자!라고 마음먹은 순간 이 글을 영원히 업로드하지 못할 것 같은 예감이 들어 얕게 알아보자고 제목을 썼는데 정말 잘했다는 생각이 든다... Event loop에 대해선 정리한 내용이 마음에 들지 않아 틈틈이 내용을 추가해야 될 것 같다. 이번 기회를 통해 Node.js를 사용하며 주의해야 될 점을 다시 생각해보게 되었고, 내가 사용하는 플랫폼 특성을 정확히 파악하는 게 에러를 방지하는 법이라는 걸 다시 한번 깨닫게 되었다. 

     


    참고 자료

    댓글

jaejade's blog ٩( ᐛ )و