Back_End/NestJS

[ 백엔드 공부하기 : Nest.js ] NestJS를 본격적으로 공부하기 전에

안다미로 : Web3 & D.S 2024. 12. 12. 18:27

 

 

 

[ 백엔드 공부하기 : Nest.js ] NestJS를 본격적으로 공부하기 전에

 


 

∇ BackEnd_NestJS : NestJS 공부 전 손풀기.

목  차

1. 웹 프레임워크
2. NodeJS
3. 이벤트 루프
4. 패키지 의존성 관리
5. TypeScript

 

 


 

Ⅰ. 웹 프레임워크.


 

▣ 웹 프레임워크의 등장 배경.

 

예전에는 웹 페이지에서 구동되는 애플리케이션이 모두 SSR(서버 사이드 렌더링) 방식으로 동작했습니다.

서버는 요청을 처리하고, 웹 브라우저가 그려야 할 HTML과 자바스크립트를 응답으로 전송했습니다.

 

브라우저는 서버에서 전달된 코드를 화면에 표시하기만 하면 되었죠.

물론, 이후 동적으로 구성되는 부분은 함께 전달된 자바스크립트를 파싱하여 화면을 구성했습니다.

 

하지만 시간이 지남에 따라 웹 기술은 점점 더 복잡해졌습니다.

웹 앱을 만들기 위해 필수적으로 적용해야 하는 기술들을 기존 방식으로 작성하는 것은

개발자에게 많은 시간과 노력을 요구하게 되었습니다.

 

이에 따라 웹 개발에 필수적인 요소들을 묶어 개발자들이 쉽게 사용할 수 있도록 하려는 시도가 생겼고,

이를 웹 프레임워크라고 부릅니다.

 

웹 프레임워크는 데이터베이스 연결 설정, 데이터 관리, 세션 유지 등의 작업을

정해진 방법과 추상화된 인터페이스로 제공합니다.

 

이러한 방법들이 프레임워크 사용자의 자유도를 제약한다고 생각할 수도 있지만,

프레임워크에서 제시하는 표준 방법을 이용하면 쉽고 빠르게 안정적인 애플리케이션을 구축할 수 있습니다.

 

프레임워크는 뼈대나 골조를 의미하는데, 미리 만들어진 뼈대를 세우고

그 위에 기능을 추가하는 작업을 하기 때문에 붙여진 이름입니다.

 

 


 

▣ 웹 프레임워크의 종류.

 

     @ 'SPA'는 SSR 방식과는 다르게, 서버로부터 매 요청에 대해 최소한의 데이터만 응답받고, 

          화면 구성 로직을 프론트엔드에서 구성합니다.

        =>> 페이지 이동 시 화면이 깜빡거리는 것과 같이 어색한 화면효과가 줄어들 수 있다는 장점이 있지만

                 초기 페이지 로딩 속도가 오래 걸린다는 단점이 있습니다.

 

 


Ⅱ. NodeJS.


 

▣ NodeJS란.

 

        ∇ Nest는 Node.js를 기반으로 동작합니다.

 

              -> 정확히는 Nest로 작성한 소스코드를 Node,js 기반 프레임워크인 Exrpess나 Fastify에서 실행 가능한

                      자바스크립트 소스코드로 컴파일해 주는 역할을 담당합니다.

                        [ nodejs의 동작원리를 이해하는게 Nest개발에 도움 ]

 

 

        ∇ Node.JS의 등장으로 자바스크립트를 이용하여 서버를 구동할 수 있게 되었습니다.

 

              -> 프론트-백에서 같은 언어를 사용한다는 것은 큰 장점.

 

 

        ∇ Node.JS는 NPM(Node Package Manager) 이라고 하는

                 패키지 (또는 라이브러리) 관리 시스템을 가지고 있음.

 

 


▣ NodeJS의 특징.

      ◎ 단일 스레드에서 구동되는 non-blocking I/O 이벤트 기반 비동기 방식.

 

 

멀티 쓰레드 방식은 작업 요청이 동시에 들어올 때 각 작업을 처리하기 위해 쓰레드를 생성하고 할당하는 방식입니다.

 

이 방식은 여러 작업을 동시에 처리할 수 있어 작업 처리 속도가 빠르다는 장점이 있지만,

공유 자원을 관리하는 데 많은 노력이 필요합니다.

 

잘못된 동기화로 인해 락에서 빠져나오지 못하는 경우도 발생할 수 있습니다.

또한, 쓰레드가 늘어날수록 메모리를 소모하게 되므로 메모리 관리도 중요합니다.

 

 

반면, Node.js는 단일 쓰레드에서 작업을 처리합니다.

애플리케이션 단에서는 단일 쓰레드로 동작하지만,

백그라운드에서는 Node.js에 포함된 libuv가 쓰레드 풀을 구성하여 작업을 처리합니다.

 

이를 통해 개발자는 단일 쓰레드에서 동작하는 것처럼 이해하기 쉬운 코드를 작성할 수 있습니다.

웹 서버를 운영할 때는 CPU 코어를 분산하여 관리하므로 실제 작업은 여러 코어에서 독립적으로 처리됩니다.

 

 

Node.js는 들어온 작업을 앞의 작업이 끝날 때까지 기다리지 않고(non-blocking) 비동기로 처리합니다.

 

이 개념을 쉽게 설명하기 위해 책에서는 푸드코트의 예를 듭니다.

푸드코트에서는 주문을 한 곳에서 받지만, 음식은 각 식당에서 개별적으로 만듭니다.

음식이 완성된 순서대로 각 식당에서 호출벨을 통해 손님을 부르고, 손님은 음식을 픽업합니다.

여기서 계산을 담당하는 작업은 단일 쓰레드이고, 각 요리를 완성해 벨을 호출하는 식당들은 비동기로 요리를 준비합니다.

 

즉, 입력은 하나의 쓰레드에서 받지만, 순서대로 처리하지 않고 먼저 처리된 결과를 이벤트로 반환하는 방식이 바로 Node.js의 단일 쓰레드 non-blocking 이벤트 기반 비동기 처리 방식입니다.

 

 

최근 ECMAScript에 새로운 기능이 추가되면서 비동기 방식으로 복잡한 기능을 구현하는 것이 더욱 간편해지고 있습니다. ECMAScript 2015(ES6)에서는 Promise가 도입되어 간결한 표현으로 비동기 처리를 할 수 있게 되었고,

ECMAScript 2017에서는 async/await 기능이 추가되어

비동기 동작을 마치 동기로 처리하는 것처럼 코드를 작성할 수 있게 되었습니다.

 


▣ NodeJS의 장단점.

      ◎ 하나의 스레드로 동작하는 것처럼 코드를 작성할 수 있다는 장점.

             -> 단일 스레드 이벤트 기반 비동기 방식은 서버의 자원에 크게 부하를 가하지 않습니다.

                     [ 대규모 네트워크 앱을 개발하기에 적합. ]

                  

      ◎ 컴파일러 언어의 처리속도에 비해 그 성능이 떨어진다는 단점.

 

 


 

Ⅲ. 이벤트 루프.


 

◇ 이벤트 루프와 Node.js의 비동기 처리

 

이벤트 루프는 시스템 커널에서 가능한 작업이 있다면 그 작업을 커널에 이관합니다.

자바스크립트가 단일 쓰레드 기반임에도 불구하고,

Node.js는 비차단 I/O 작업을 수행할 수 있도록 해주는 핵심 기능입니다.

 

이벤트 루프는 총 6개의 단계(Phase)로 구성되어 있으며,

각 단계는 처리해야 할 콜백 함수를 담기 위한 큐를 가지고 있습니다.

 

아래 그림에서 루프를 이루고 있는 부분은 네모박스로 표시되어 있습니다.

화살표는 각 단계가 전이되는 방향을 나타내지만, 반드시 다음 단계로 넘어가는 것은 아닙니다.

자바스크립트 코드는 idle & prepare 단계를 제외한 어느 단계에서나 실행될 수 있습니다.

 

 

  nextTickQueue와 microTaskQueue

 

nextTickQueue와 microTaskQueue는 이벤트 루프의 구성 요소는 아니지만,

이 큐에 들어 있는 작업은 이벤트 루프가 어느 단계에 있든지 실행될 수 있습니다.

 

 

  Node.js 애플리케이션 실행 과정

 

node main.js 명령어로 Node.js 애플리케이션을 콘솔에서 실행하면,

Node.js는 먼저 이벤트 루프를 생성한 다음 메인 모듈인 main.js를 실행합니다.

 

이 과정에서 생성된 콜백들은 각 단계에 존재하는 큐에 들어가게 되며,

메인 모듈의 실행을 완료한 후 이벤트 루프를 계속 실행할지 결정합니다.

 

만약 큐가 모두 비어 더 이상 수행할 작업이 없다면, Node.js는 루프를 빠져나가고 프로세스를 종료합니다.

 

 

          

 


 

▣ Timer 단계.

이벤트 루프는 Timer 단계에서 시작합니다.

이 단계의 큐에는 setTimeout이나 setInterval과 같은 함수를 통해 생성된 타이머들이 들어가고 실행됩니다.

타이머는 now - registeredTime ≥ delta 조건을 만족하는 값들로 큐에 추가됩니다.

 

여기서 delta는 setTimeout(() => {}, delta) 과 같이 타이머가 등록된 시각에서 얼마나 시간이 흐른 후에

동작해야 하는지를 나타내는 값입니다.

 

즉, 대상 타이머들은 이미 실행할 시간이 같거나 지났다는 의미입니다.

 

타이머들은 최소 힙(Min Heap)으로 관리되며, 이는 최솟값을 찾기 위해 완전 이진 트리를 사용하는 자료 구조입니다.

힙을 구성할 때, 실행할 시각이 가장 적게 남은 타이머가 힙의 루트가 됩니다.

Timer 단계에서는 최소 힙에 들어 있는 타이머들을 순차적으로 찾아 실행한 후, 힙을 재구성합니다.

 

예를 들어, 딜레이 값이 100, 200, 300, 400인 4개의 타이머 A, B, C, D를 특정 시간 t에 힙에 등록했다고 가정해봅시다.

이 경우 최소 힙은 A → B → C → D의 순서로 구성됩니다.

 

이제 이벤트 루프가 t+250 시각에 Timer 단계에 진입했다고 가정하면,

힙에서 A, B, C, D를 순차적으로 꺼내어 시간을 비교합니다.

 

A와 B는 이미 250만큼의 시간이 지났기 때문에 이들의 콜백은 수행되지만,

C는 아직 시간이 지나지 않았기 때문에 실행되지 않습니다. D는 최소 힙의 특성상 C를 이미 실행하지 않기로 했기 때문에 비교할 필요가 없습니다.

 

또한, 시간이 지난 타이머들의 콜백이 무한정 실행되는 것은 아니며,

시스템의 실행 한도(Hard Limit)에 도달하면 다음 단계로 넘어갑니다.

 

 

 

▣ Pending (I/O) 콜백 단계

이 단계의 큐에 들어있는 콜백들은 현재 돌고 있는 루프 이전의 작업에서 큐에 들어온 콜백입니다.

예를 들어 TCP 핸들러 내에서 비동기의 쓰기 작업을 한다면,

TCP 통신과 쓰기 작업이 끝난 후 해당 작업의 콜백이 큐에 들어옵니다.

또 에러 핸들러 콜백도 pending_queue 로 들어오게 됩니다.


Timer 단계를 거쳐 pending 콜백 단계에 들어오면 이전 작업들의 콜백이 pending_queue 에서 대기중인지를 검사합니다.

만약 실행 대기 중이라면 시스템 실행 한도에 도달할 때까지 꺼내어 실행합니다.

 

 

 

▣ Idle, Prepare 단계

Idle 단계는 매 틱(Tick, 매 단계가 이동하는 것을 의미함)마다 실행됩니다.

Prepare 단계는 매 폴링마다 그 전에 실행된다. 이 두 단계는 Node.js 의 내부 동작을 위한 것 입니다.

 

 

 

▣ Poll 단계

이벤트 루프 중 가장 중요한 단계는 Poll 단계입니다. 이 단계에서는 새로운 I/O 이벤트를 가져와 관련된 콜백을 수행합니다. 예를 들어, 소켓 연결과 같은 새로운 연결을 맺거나 파일 읽기와 같은 데이터 처리를 수행합니다.

 

Poll 단계에서 사용하는 큐는 watch_queue입니다. 이 단계에 진입한 후, watch_queue가 비어 있지 않다면, 큐가 비거나 시스템 실행 한도에 도달할 때까지 동기적으로 모든 콜백을 실행합니다.

만약 큐가 비게 되면, Node.js는 즉시 다음 단계로 이동하지 않고,

check_queue(Check 단계의 큐), pending_queue(Pending 콜백 단계의 큐),

closing_callbacks_queue(Close 콜백 단계의 큐)에 남아 있는 작업이 있는지 검사합니다.

작업이 있다면 다음 단계로 이동하고, 모든 큐가 비어 있어 해야 할 작업이 없다면 잠시 대기하게 됩니다.

 

이때 대기 시간은 타이머 최소 힙의 첫 번째 타이머를 꺼내어, 지금 실행할 수 있는 상태라면 그 시간만큼 대기한 후

다음 단계로 이동합니다.

 

이렇게 하는 이유는 타이머 단계로 넘어가더라도 첫 번째 타이머를 수행할 시간이 되지 않았기 때문에,

이벤트 루프를 한 번 더 돌아야 하므로 Poll 단계에서 시간을 보내는 것입니다.

 

 

▣ Check 단계

Check 단계는 setImmediate 의 콜백만을 위한 단계입니다.

역시 큐가 비거나 시스템 실행 한도에 도달할 때 까지 콜백을 수행합니다.

 

 

▣ Close 콜백 단계

socket.on('close', () => {}) 과 같은 close  destroy 이벤트 타입의 콜백이 여기서 처리됩니다.

이벤트 루프는 close 콜백 단계를 마치고 나면 다음 루프에서 처리해야 하는 작업이 남아 있는지 검사합니다.

만약 작업이 남아 있다면 Timer 단계부터 한 번 더 루프를 돌게 되고 아니라면 루프를 종료합니다.

 

 

 

▣ nextTickQueue과 microTaskQueue

nextTickQueue  process.nextTick() API 의 콜백들을 가지고 있으며,

 microTaskQueue 는 resolve 된 promise 의 콜백을 가지고 있습니다.

이 두개의 큐는 기술적으로 이벤트 루프의 일부가 아닙니다.

 

즉, libuv 라이브러리에 포함된 것이 아니라 Node.js 에 포함된 기술입니다.

이 두 큐에 들어 있는 콜백은 단계를 넘어가는 과정에서 먼저 실행됩니다. 

 

nextTickQueue  microTaskQueue 보다 높은 우선순위를 가지고 있습니다.

정리하자면 다음과 같은 워크플로우를 가집니다.

 

 


 

Ⅳ. 패키지 의존성 관리.


 

▣ package.json

      ● 애플리케이션이 필요로 하는 패키지 목록을 나열합니다.

      ● 각 패키지는 시맨틱 버저닝 규칙으로 필요한 버전을 기술합니다.

      ● 다른 개발자와 같은 빌드환경을 구성하여, 의존성이 달라 발생하는 문제를 예방합니다.

 

 

▣ 시맨틱 버저닝.

       :: Node.js에서 사용되는 "시맨틱 버저닝 규칙" 은 패키지의 버전명을 숫자로 관리하는 방법입니다.

 

           *버저닝 규칙.

<Major>.<Minor>.<Patch>-<label>

 

      ● Major, Minor, Patch 는 각각 숫자를 사용합니다.

 

      ● Major : 이전 버전과 호환이 불가능할 때 숫자를 하나 증가합니다.

                       Major 버전이 바뀐 패키지를 사용하고자 한다면 반드시 breaking change(하위 호환성이 깨진 기능)

                       목록을 확인하고 이전 기능을 사용하는 코드를 수정해야 합니다.

 

      ● Minor: 기능이 추가되는 경우 숫자를 증가합니다.

                      기능이 추가되었다고 해서 이전 버전의 하위 호환성을 깨뜨리지는 않습니다.

 

      ● Patch: 버그 수정 패치를 적용할 떄 사용합니다.

 

      ● label(선택사항): pre, alpha, beta와 같이 버전에 대해 부가 설명을 붙이고자 할 때, 문자열로 작성합니다.

 

 

시맨틱 버저닝을 사용할 때 완전히 동일한 버전만을 정의하지 않습니다.

 다음과 같은 규칙으로 기술하여 의존성이 깨지지 않는 다른 버전을 설치할 수 있습니다.

  • ver: 완전히 일치하는 버전
  • =ver: 완전히 일치하는 버전
  • >ver: 큰 버전
  • >=ver: 크거나 같은 버전
  • <ver: 작은 버전
  • <=ver: 작거나 같은 버전
  • ~ver: 버전범위
    • ~1.0, 1.0.x: 1.0 이상 1.1 미만의 버전
  • ^ver: SemVer 규약을 따른다는 가정에서 동작하는 규칙
    • ^1.0.2: 1.0.2 이상 2.0 미만의 버전
    • ^1.0: 1.0.0 이상 2.0 미만의 버전
    • ^1: 1.0.0 이상 2.0 미만의 버전

 

 

▣ package-lock.json

 

프로젝트 루트 디렉토리에서 npm install 명령을 실행하면

node_modules 디렉토리와 package-lock.json 파일이 생성됩니다.

 

package-lock.json 파일은 node_modules나 package.json 파일의 내용이 변경될 때마다 npm install 명령을 수행할 때

자동으로 수정됩니다.

 

node_modules는 프로젝트가 필요로 하는 패키지들이 실제로 설치되는 장소이며,

애플리케이션은 런타임에 이곳에 설치된 패키지들을 참조합니다.

 

package-lock.json 파일은 package.json에 선언된 패키지들이 설치될 때의 정확한 버전과 서로 간의 의존성을 표현합니다.

 

이를 통해 팀원들 간에 정확한 개발 환경을 공유할 수 있으며,

같은 패키지를 설치하기 위해 node_modules를 소스 리포지토리에 공유할 필요가 없습니다.

 

 

만약 소스 코드 내에 package-lock.json 파일이 이미 존재한다면,

npm install 명령을 수행할 때 이 파일을 기준으로 패키지들이 설치됩니다.

 

따라서 package-lock.json 파일을 소스 코드 리포지토리에서 관리하는 것이 중요합니다.

 

 


Ⅴ. TypeScript.


 

TypeScript 개요

 

  • 개발사: TypeScript는 마이크로소프트에서 개발한 프로그래밍 언어입니다.
  • 특징:
    • JavaScript 코드에 타입 시스템을 도입하여, 런타임에서 발생할 수 있는 에러를 정적 분석을 통해 사전에 찾아냅니다.
    • JavaScript에 구문을 추가하여 만들어졌습니다.
  • 타입 추론:
    • TypeScript는 타입 추론 기능을 제공하여, 타입 오류로 인해 런타임에서 발생할 수 있는 오류를 컴파일 타임에 잡아냅니다.

 

 

 

▣ 변수 선언.

      - TS에서 변수를 선언하는 방식은 다음과 같습니다.

<선언 키워드> <변수명>: <타입>;

 

   ● 선언 키워드: const, let 또는 var 로 선언.

      

        ○ const 는 선언 후 재할당이 불가능, let  var 는 재할당이 가능하여 값을 바꿀 수 있음.

        ○let  var 의 차이는 hoisting 여부인데, var 는 변수를 사용한 후에 선언이 가능하지만 let 은 그렇지 못함

 

 

▣ TypeScript에서 지원하는 타입,

타입스크립트는 자바스크립트가 가지고 있는 자료형을 모두 포함합니다.

 

자바스크립트의 타입은 기본 타입(primitive value)과 객체형(object), 함수형(function)이 있습니다.

 

아래와 같이 typeof 키워드를 이용하여 인스턴스의 타입을 알 수 있습니다.

typeof instance === "undefined"

 

 

    ◎ primitive 타입.

       

 

    ◎ object 타입.

 

         객체 타입은 속성(property)을 가지고 있는 데이터 컬렉션입니다.

 

         속성은 키와 값으로 표현 되는데 값은 다시 javascript 의 타입을 가지고 있습니다.

 

         따라서 다음 예와 같이 데이터를 구조적으로 표현할 수 있습니다.

 

const dexter = {
  name: 'Dexter Han',
  age: 21,
  hobby: ['Movie', 'Billiards'],
}

 

 

    ◎ function 타입.

 

        - JS는 함수를 변수에 할당하거나, 다른 함수의 인자로 전달 가능합니다.

        - 함수의 결과로 반활할 수도 있습니다. [일급함수]

 

 

 

    ◎ any/ unknown / never

 

  • any: JavaScript와 마찬가지로 어떤 타입의 값도 받을 수 있는 타입입니다.
      any 타입의 객체는 어떤 타입의 변수에도 할당이 가능하지만, 이로 인해 런타임에 오류가 발생할 가능성이 있습니다.

  • unknown: any 타입과 유사하게 어떤 타입도 할당할 수 있지만,
                     다른 변수에 할당하거나 사용할 때 타입을 강제하도록 하여 any가 일으킬 수 있는 오류를 줄여줍니다.

  • never: 이 타입의 변수에는 어떤 값도 할당할 수 없습니다.
               함수의 리턴 타입으로 지정하면 해당 함수가 어떤 값도 반환하지 않는다는 것을 의미하며,
                특정 타입의 값을 할당받지 못하도록 하는 데에도 사용될 수 있습니다.

 

 

▣ 타입 정의하기.

typescript 는 타입을 정의해서 사용할 수 있다. 기본 타입과 같은 타입을 정의한다는 뜻은 아니고,

위에서 설명한 타입들을 조합하여 타입에 이름을 붙여 사용합니다.


vscode 는 아래와 같이 마우스를 변수 위로 가져가면 추론된 타입을 표시해줍니다.

 

 

 

변수에 객체를 바로 할당하지 않고 interface 로 정의할 수 있습니다.

아래는 인터페이스를 이용하여 User 타입을 선언하는 것입니다.

interface User {
    name: string;
    age: number;
}

const user: User = {
    name: 'Dexter',
    age: 21,
}

 

interface 는 class 로 선언할 수도 있습다.

class User {
  constructor(name: string, age: number) { }
}

const user: User = new User('Dexter', 21);

 

또 타입은 type 키워드로 새로운 타입을 만들 수 있습다.

 

 

 

▣ 타입 구성하기.

javascript 는 변수에 어떠한 타입의 값도 할당할 수 있습니다.

typescript 도 여러 타입의 값을 할당할 수 있습니다. 여러 타입을 조합한 새로운 타입을 가지는 것 입니다.

 

    ◎ union 타입.

        : 여러 타입을 조합한 타입,

        union 타입을 활용하면, 변수가 가질 수 있는 값을 제한할 수도 있습니다.

 

function getLength(obj: string | string[]) {
  return obj.length;
}

 

enum Status {
  READY = "Ready",
  WAITING = "Waiting",
}

 

 

 

    ◎ generic타입.

          : 어떤 타입이든 정의될 수 있지만, 호출되는 시점에 타입이 결정됩니다.

function identity(arg: any): any {
  return arg;
}

 

이 함수의 반환값은 any 로 되어 있기 때문에

arg 에 'test'를 인자로 전달할 경우 전달한 인자의 string 타입이 반환할 때 any가 되어 버립니다.


반면 다음과 같이 generic 타입을 사용하게 되면 리턴되는 값의 타입은 함수를 호출하는 시점의 인자로 넣은 타입으로

결정되도록 할 수 있습니다. 제네릭을 선언할 때는 보통 대문자 한글자를 사용합니다.  

 

function identity<T>(arg: T): T {
  return arg;
}