노드는 자바스크립트 실행기로 자바스크립트 문법을 익히면 노드를 활용 할 수 있다. 기존의 자바스크립트는 웹 브라우저에서 그저 약간의 동적 처리를 위해서 존재했다. 하지만 2008년 구글에서 V8엔진을 사용해 크롬을 출시하고, 오픈소스로 코드를 공개했는데, 기존의 다른 엔진보다 속도가 월등히 빨랐다. 이전에도 자바스크립트를 브라우저 외에서 사용하고자 하는 시도가 있었으나 속도 문제로 적용을 못하던 이슈를 해결하게 되었고, 이로 인해 2009년에 노드가 탄생하게 됨.
Node.js: 크롬 V8 자바스크립트 엔진으로 빌드된 자바스크립트 runtime. 노드를 통해 서버를 구성하거나 자바스크립트 프로그램을 실행하는 런타임으로 사용할 수 있다. runtime: 특정 언어로 만든 프로그램을 실행할 수 있는 환경 server: 네트워크를 통해 클라이언트로 정보, 서비스를 제공하는 컴퓨터나 프로그램들을 총칭한다. client: 요청을 보내는 주체를 말한다. 브라우저(웹), 앱, 프로그램이나 또 다른 별도의 서버들도 클라이언트가 될 수 있다.
Node.js의 구조
노드는 V8, libuv(라이브러리)를 사용하는데, C, C++ 로 구현되었고, 해당 언어는 몰라도 된다. libuv는 노드의 특성인 이벤트 기반, 논블로킹 I/O 모델을 구현한다.
V8: 구글 크롬 브라우저, 안드로이드 브라우저에 탑재된 자바스크립트 엔진 libuv: 비동기I/O에 중점을 두고 있는 라이브러리로 Node.js용으로 만들어졌으나 현재는 크로스 플랫폼을 지원한다. 스레드 풀을 사용하여 비동기I/O를 작업하지만 네트워크 I/O는 OS에서 처리한다.
이벤트 기반
이벤트가 발생하면 미리 지정한 작업을 수행하는 방식들로 addEventListener를 통해 등록한 클릭이나 키 이벤트 행위나 네트워크 요청들을 말한다. 이벤트가 발생하면 어떤 행위를 할지 미리 등록해야하는데, 이런 행위를 이벤트 리스너에 콜백 함수를 등록한다고 표현한다. 이벤트 기반 모델에서는 이벤트 루프라는 개념이 있는데, 동시에 이벤트가 일어나면 어떤 행위를 순차적으로 호출할지 판단해야하는데 이벤트 루프가 해당 역할을 합니다. 자바스크립트는 기본적으로 맨 위에서 아래로 실행되며 한줄씩 실행되고 호출 해야할 함수를 만나면 call stack영역에 담아두고 실행되는 동안 존재하다가 실행되면 사라진다.
📄call stack example1.js
function one(){
two();
console.log("one");
}
function two(){
three();
console.log("two");
}
function three(){
console.log("three");
}
one();
// call stack
// 1. one() 함수가 call stack에 쌓임
// 2. two() 함수가 call stack에 쌓임
// 3. three() 함수가 call stack에 쌓임
// run
// 1. three 출력
// 2. two출력
// 3. one출력
// 각 함수별 처리때문에 실행은 call stack에 쌓인것과 반대로 출력된다.
📄call stack example2.js
function run(n){
console.log("3초 뒤 실행");
}
console.log("start!");
setTimeout(run, 3000);
console.log("end!");
start!, end!, 3초 뒤 실행이 찍히는 걸 볼 수 있는데, 실제 호출 스택은 아래와 같이 동작한다.
호출스택에 console.log("start!")가 쌓이고 테스크 큐에 의해 바로 실행된다.
호출스택에 setTimeout() 메소드가 쌓이고 백그라운드로 넘긴다.
호출스택에 console.log("end!")가 쌓이고 테스크 큐에 의해 바로 실행된다.
백그라운드에서 3초가 지나면 테스크 큐로 run메소드를 넘긴다.
테스크 큐에서 run을 실행하면서 console.log("3초 뒤 실행")이 출력된다.
event loop: 이벤트가 발생하면 호출될 콜백 함수들을 관리하고 실행순서를 결정해주는 역할을 한다. background: setTimeout같은 타이머, 이벤트 리스너들이 대기하는 곳으로 여러 작업이 동시에 일어날 수 있다.(setTimeout시간이 되어 실행되면서 클릭도 동시에 일어날 수 있음) task queue: 이벤트가 발생하고 백그라운드에서 테스크 큐로 타이머나 이벤트의 콜백이 넘어오는 곳 정해진 순서대로 실행되지만 특정한 경우에는 순서가 바뀔 수도 있다.
논블로킹 I/O
작업 종류는 두종류가 있는데, 동시에 실행 가능한 작업과 동시에 실행이 불가능한 작업이 존재하는데, 일반적으로 우리가 작성한 코드들은 동시에 실행될 수 없다. 여기서 동시에 실행가능한 것은 무엇일까? 바로 I/O작업을 말하는데, 노드에서 논블로킹 방식으로 제공한다. Input/Output: 입출력을 말하며, 파일 시스템에 접근(파일을 읽거나 쓰기)하거나 네트워크 작업 등 Blocking: 이전 작업이 있으면 끝날때까지 기다리다가 이전 작업이 끝나야 비로소 현재 작업이 수행 됨. Non Blocking: 이전 작업을 신경쓰지않고 동시에 작업들을 수행한다.
한 작업당 1초가 걸리는데, 10개의 작업이 있다면 블로킹으로 처리시 10초가 걸린다. 하지만 논블로킹이라면? 10초보다는 훨씬 줄어들어서 완료가 된다. setTimeout(callback, 0)형태로 논블로킹을 간접 체험 할 수 있는데, 아무리 논블로킹 형태로 작성하더라도 직접 작성하면 전체 소요시간이 짧아 지진 않는다. 직접 작성한 코드들은 동시에 실행되지 않기 때문에, I/O작업에서 효율을 볼 수 있다.
싱글 스레드
싱글 스레드란 말그대로 스레드가 한개라는 뜻이다. 우리가 작성한 코드들이 동시에 실행되지 않는 이유가 노드는 싱글 스레드이기 때문이다. (하지만 내부를 자세히 살펴보면 노드를 실행하면 하나의 프로세스가 생성되고, 그 프로세스 안에는 여러개의 스레드가 존재하는데, 우리가 직접 제어하는 스레드가 한개뿐이다.)
멀티 스레드라는 말도 있는데, 이건 반대다 여러개의 작업을 동시에 실행할 수 있다.(ex) java) 그렇다면 멀티 스레드를 사용하는게 무조건 좋을까? 그건 또 아니다. 아래는 김밥천국을 예시로 들어본다.
싱글, 멀티스레드별 블로킹, 논블로킹
-싱글스레드&블로킹({직원 1명, 고객 3명}, {직원:thread, 고객: response}) 고객 3명이 직원 1명에게 들어온 순서대로 주문을 요청하고 첫번째 음식이 나오면 다음 손님 주문을 받고 음식이 나오면 제공한다. 지연시간이 길어진다.
-싱글스레드&논블로킹({직원 1명, 고객 3명}, {직원:thread, 고객: response}) 고객 3명이 직원 1명에게 동시에 주문을 요청하고 음식이 나오면 직원 1명이 고객 3명에게 주문에 알맞은 음식을 제공한다. 지연시간이 대폭 줄어든다.
-멀티스레드&블로킹({직원 3명, 고객 3명}, {직원:thread, 고객: response}) 직원이 3명이므로 고객 1명당 직원이 1명씩 각각 붙어서 각 메뉴를 주문받고 완료된 요리를 제공한다. 빠르지만 효율적이지 않다.
언뜻 보기엔 좋아보이지만 손님에 맞춰 직원이 들어나다보니 비용이 많이(자원 낭비) 듭니다. 손님이 자리를 떠나면 놀고 있는 직원들이 많아질 수 있습니다. 즉, 멀티스레드이면서 논블로킹으로 작성하면 효율적으로 할 수 있지만 이런 방식의 프로그래밍은 어려운 방법으므로 멀티 프로세싱 방식을 사용한다.
서버로서의 Node.js
노드는 기본적으로 싱글 스레드&Non blocking 모델을 사용한다.(JS 특성) 서버이기 때문에 자연스럽게 클라이언트의 I/O의 요청 작업이 많은데, libuv라이브러리를 사용해 I/O 작업을 논블로킹 방식으로 처리한다. 따라서 스레드 하나가 많은 수의 I/O작업을 감당한다. 하지만 CPU부하가 많은 작업을 하게되면 서버가 죽어버릴 수 있기 때문에 위험하다. 그렇기때문에 이미지, 비디오처리, 대규모 데이터 처리와 같은 CPU에 부하 작업이 많은 곳에선 서버로 권장하지 않는다. 이 외에도 싱글 스레드로 동작하다보니 에러를 제대로 처리하지 못하는 경우에도 하나뿐인 스레드가 죽으면서 서버가 죽고 전체 서비스가 동작하지 않는 현상도 일어날 수 있다.
장점
단점
적은 자원으로 서버를 구성
싱글 스레드라 부담이 크다. CPU 코어를 하나만 씀
I/O작업이 많은 서버에 좋음
CPU작업에 부하가 간다면 부적합
멀티 스레드보다 쉬움
하나뿐인 스레드 관리 필요
웹 서버가 내장
규모가 커지면 nginx와 연결해야함
JS를 사용함(비교적 쉽다)
성능이 애매함
JSON과 호환된다
-
서버외의 노드
노드의 사용범위가 늘어나면서 웹, 모바일 앱, 데스크톱 앱 개발에도 사용되기 시작했다. 대표적으로 리액트, 앵귤러, 뷰가 존재한다. 모바일 앱에는 리액트 네이티브가 있고 데스크톱 앱을 개발할땐 일렉트론을 사용한다. 웹 개발을 하면서 많이 사용하는 도구인 Atom, vs code도 일렉트론으로 만들어졌다.
NVM
노드를 사용하려면 먼저 Node.js를 공식홈페이지에서 다운로드를 받아서 사용하면 된다. 이렇게 되면 하나의 노드 버전을 사용해서 개발을 하는데, nvm을 설치해서 사용하게 되면 여러 버전의 노드를 사용할 수 있다. (기존의 노드를 삭제하지 않고 약간의 명령어를 통해 사용하려는 노드버전을 설치하고 바꾼다.)
//example
type T0 = Exclude<"a" | "b" | "c", "a">;
// "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"
type T2 = Exclude<string | number | (() => void), Function>;
// string | number
🔹 Extract
Extract<T, U> T에서 U만 추출한다.(Exclude의 반대)
//example
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// "a"
type T1 = Extract<string | number | (() => void), Function>;
// () => void
🔹 NonNullabble
NonNullabble<T> 타입 중 null과 undefined를 제외한다.
//example
type T0 = NonNullabble<string | number | undefined>;
// string | number
type T1 = NonNullabble<string[] | null | undefined>;
// string[]
🔹 Parameters
Parameters<T> T의 값(매개변수)들이 튜플(타입이 정확히 지정된 배열의 타입) 타입으로 구성된다.
//example
declare function f1(arg: { a: number; b: string }): void;
type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [string]
type T2 = Parameters<<T>(arg: T) => T>; // [unknown]
type T3 = Parameters<typeof f1>; // [{a: number, b:string}]
type T4 = Parameters<any>; // unknown[]
type T5 = Parameters<never>; // never
// (Error) Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T6 = Parameters<string>;
// (Error) Type 'Function' does not satisfy the constraint '(...args: any) => any'.
type T7 = Parameters<Function>;
🔹 ConstructorParameters
ConstructorParameters<T> 생성자 함수 유형의 유형에서 튜플 또는 배열 유형을 생성합니다. 모든 매개변수 유형(또는 함수가 아닌 never경우 유형)이 포함된 튜플 유형을 생성합니다 . --> ts 공식문서 인용
//example
type T0 = ConstructorParameters<ErrorConstructor>; // [message?: string]
type T1 = ConstructorParameters<FunctionConstructor>; // string[]
type T2 = ConstructorParameters<RegExpConstructor>; // [pattern: string | RegExp, flags?: string]
interface Inew {
new (arg: string): Function;
}
type T3 = ConstructorParameters<Inew>; // [string]
function f1(a: T3) {
a[0], a[1]; // (Error) Tuple type '[arg: string]' of length '1' has no element at index '1'
}
🔹 ReturnType
ReturnType<T> 함수 T의 반환 타입으로 구성된 타입을 생성한다.
//example
declare function f1(): { a: number; b: string };
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<typeof f1>; // {a: number;b: string;}
type T5 = ReturnType<any>; // any
type T6 = ReturnType<never>; // never
// (Error) Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T7 = ReturnType<string>;
/* (Error) Type 'Function' does not satisfy the constraint '(...args: any) => any'.
Type 'Function' provides no match for the signature '(...args: any): any'. */
type T8 = ReturnType<Function>;
🔹 Required
Required<T> 기존 정의된 프로퍼티가 옵셔널(?)로 필수값이 아니더라도 T의 모든 프로퍼티가 필수로 사용하도록 설정된다.
//example
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
// (Error) Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.
const obj2: Required<Props> = { a: 5 };
ES3, ES5, ES6, ES2022 등 최신 문법까지 작성가능하며, 보통은 모든 브라우저에서 ES6를 지원하기때문에, ES6를 쓰는것을 추천합니다. 너무 낮은 버전인 ES3로 작성하게되면 컴파일 후 작성된 JS의 양이 많이질 수 있습니다. 하지만 서버용으로 개발하면서 낮은 버전의 모든것을 호환하려면 ES3를 생각해보는것도 괜찮습니다.
그렇다고 또 최신의 ES2022의 버전 또한 모든 브라우저가 제공되는게 아닐 수 있기때문에, 안정적인 버전으로 사용하는게 좋습니다.
{
"compilerOptions": {
"target": "ES6"
}
}
2-3. lib
lib은 개발시 개발환경의 코드를 지정하는 옵션입니다. 웹개발을 통한 브라우저의 정보를 받고 싶다면, DOM을 입력하여 (document.~, window.~)의 정보들을 받아서 사용할 수도 있고, 또한 어떤 버전의 API의 정보들을 받아서 사용하는지 지정할 수 있습니다. 백엔드 작업이 아닌 프론트 & 브라우저 작업이라면 꼭 DOM을 입력하여 개발을 진행하고, 해당 옵션이 없다면 그저 JS 자체의 코드만 버전에 맞춰 제공됩니다.
가장 멋진점은 원하는 버전을 작성하면, Javascript이지만 Typescript에 맞춰 call signature를 제공해주기 때문에, 자주 사용하지 않는 메소드등을 사용하더라도 typescript의 도움을 받을 수 있습니다.
그리고 가장 큰차이는 interface의 경우 object, class의 모양을 특정하는데만 사용된다.
주로 react에서 자주 봤던 이유이다.(api등이나 그리려는 props들의 정보를 object형태로 자주 담아서 사용했다.
즉, 바로 아래 표현한거처럼 interface는 간단한 자료형 정의와 같은 행위는 불가능하다.
interface Hello = string; //Error
상속 표현 차이점
- type
type User = {
name: string
}
type Player = User & {
}
const nico: Player = {
name: "nico"
}
- interface
interface User {
name: string
}
interface Player extends User {
}
const shin: Player = {
name: "shin"
}
부모 속성으로 부터의 상속도 가능한데 아래와 같은 차이가 있다.
1. type은 &(and) 기호를 통해 가능하다.
2. interface는 extends 키워드로 상속을 받아서 사용한다.
같은 이름의 interface가 존재하면?
또한 interface에서만 가능한 기술로 같은 파일시스템 내에서 같은 이름의 interface를 중복으로 선언하면, 하나의 인터페이스로 합쳐준다.
interface User {
nickname: string,
}
interface User {
hp: number,
}
interface User {
mp: number,
}
const warrior: User = {
nickname: "shin",
hp: 100,
mp: 40
}
알아서 3가지의 타입을 합쳐서 가지고 있다. 이런 표현방식은 interface에서만 가능하다.
interface를 class에 상속하기
abstract을 통해 상속받는 class모습을 추상적으로 그려주고 필수적으로 작성해야하는 함수를 지정하거나 할수 있는데, 이런 형태를 interface를 통해서도 가능하다.
interface User {
nickname: string,
level: number,
myInfo(): string,
sayHi(name: string): string,
}
interface Human{
health: number
}
class Player implements User,Human {
constructor(
public nickname: string,
public level: number,
public health: number
) { };
myInfo() {
return `${this.nickname}이며, ${this.health}의 체력을 가지고 있습니다.`;
}
sayHi(name: string) {
return `hello ${name}`;
}
}
다만 상속받는 class의 생성자부분에서 접근제한자는 public으로만 생성 가능하게 됩니다.
자바스크립트로 변환될때의 이점으로 interface로 정의한 User, Human의 정보의 추적이 불가능합니다. interface라는 형태는 typescript에서만 존재하기 때문에 좀 더 가벼운 소스를 만들수 있고, extends는 하나의 추상클래스만 상속가능하지만 implments는 여러개의 interface를 상속받을수 있습니다.