티스토리 뷰
Node.js의 특징
1. JS runtime 환경에서 동작한다.
2. Single Thread 이다.
3. Non-blocking I/O 이다.
4. Event-driven 방식이다.
(참고로 2, 3, 4번은 JS의 특징이기도 하다.)
대표적으로 위 4가지의 특징을 가지고 있다. 이를 기반으로 하나씩 설명을 해보려고 한다.
1. JS runtime 환경에서 동작한다.
-Node에서 JS를 사용할 수 있다는 의미이다. Node.js는 JS를 브라우저 밖에서 사용할 수 있는데 이 특징 덕분에 가능한 것이다.
-이렇게 JS를 브라우저 밖에서 사용할 수 있기 때문에 서버를 만들 수 있게 된다. JS는 클라이언트를 만들 때도 사용 가능하기 때문에 JS와 Node.js를 통해 클라이언트와 서버를 같은 언어를 사용해서 만들 수 있는 큰 장점을 가지게 된다.
2. Single Thread이다.
-Thread가 1개가 존재한다는 의미이다. 즉 한 번에 한 가지의 일을 수행할 수 있다. 하지만 Single Thread라는 말은 완벽히 맞는 말은 아니다. 아래에 그 이유를 설명하려고 한다.
코드를 실행할 때 동기적인 코드를 담당하는 어플리케이션 파트는 JS, Node.js의 엔진 내부에 존재한다. 그리고 비동기적인 코드를 담당하는 APIs 파트는 엔진 외부에 존재한다.
엔진 내부에 있는 Main Thread는 Single Thread가 맞다. 하지만 외부에서 비동기적인 코드를 담당하는 스레드는 Multi Thread이다. Single Thread는 한 번에 1가지의 일만 처리할 수 있고, Mulit thread는 병렬적으로 동시에 여러가지의 일을 처리할 수 있다.
JS의 경우 web worker를 통해, Node.js의 경우 worker thread를 통해 엔진 내부에 스레드를 여러개 생성할 수 있다. 그렇다고 single thread가 multi thread로 변하는 것은 아니다. 단지 병렬적으로 일을 처리할 수 있는 스레드가 생기는 것이다.
(JS와 Node.js 엔진 자체는 single thread로서 main thread가 되어 한 번에 하나의 작업만 처리할 수 있지만, 엔진 외부에서 비동기 코드를 처리할 수 있도록 함으로써 single thread의 단점을 보완했다.)
=> 한 마디로 JS와 Node.js의 엔진 자체는 single thread가 맞지만, 엔진 외부에서 동작하는 APIs 파트에서는 mulit thread로 동작한다. 그리고 web worker와 worker thread를 통해 엔진 내부에 스레드를 추가적으로 생성할 수 있다.
(I/O와 같은 작업은 수월하게 할 수 있으나, CPU-bound와 같은 복잡한 작업을 할 때는 엔진 외부에 있는 multi thread만으로는 제한적인 성능을 보이게 될 것이다. 이 때 web worker, worker thread를 사용하거나 / 작업을 작은 단위로 분할하여 처리를 하면 된다.)
-그렇다면 스레드가 여러개 있을 때가 무조건 더 좋은 것이 아니냐 라고 생각할 수 있다. 하지만 꼭 그런 것은 아니다.
스레드가 동작하기 위해 필요한 정보들을 스레드 마다 개별적으로 하나씩 만들어줘야 하기 때문에 이에 따른 메모리 사용량이 증가할 수 있고, 많아지는만큼 CPU에서 스케줄링을 할 때도 비용이 더 발생하게 된다.
(대표적으로 애초에 여러개의 스레드를 두고 사용하는 프로그래밍 언어는 대표적으로 JAVA가 있다.)
*참고로 Node.js의 특징 중 Non-blocking I/O와 Event-Driven이 존재하는데 이들은 Single Thread의 단점을 보완해주는 역할을 해준다.
3. Non-blocking I/O이다.
-Non-blocking I/O에서 I와 O는 input과 output을 뜻하는데, 컴퓨터에서 file을 읽고 쓰거나 데이터베이스를 읽고 쓰거나 network에 요청와 응답을 받는 즉 컴퓨터의 하드웨어적인 것에 대해 읽고 쓰는것을 뜻한다.
(데이터베이스도 일종의 file 형태이다.) (I/O와 상반되는 개념은 CPU이다.)
-뒤에서 설명할 내용인데, Node.js는 I/O 작업을 '비동기'로 처리하게 되는데 이 때 비동기 코드들은 Node.js의 '내부 엔진'이 아닌, '외부 엔진'인 APIs 파트에서 처리가 된다. 이렇게 비동기 코드를 외부 엔진에서 처리하는 동안, 동기적인 코드를 담당하는 내부 엔진이 동기적인 코드를 해석하고 처리하는 작업에 영향을 주지 않는다는 뜻이다.
=> 즉, Non-blocking I/O는 비동기적인 코드와 동기적인 코드가 서로 실행되는데에 영향을 주지 않는다는 뜻이다. 한 마디로 비동기적인 코드를 사용할 수 있게 해주는 역할을 한다.
4. Event-Driven의 역할
-개발자가 미리 만들어놓은 어떠한 이벤트가 발생하면 특정 함수가 실행되게 해줌으로써 동적으로 프로그래밍을 구성할 수 있게 해주는 역할을 한다.
(예를 들어 어떤 버튼을 클릭하는 이벤트가 발생하면 경고창을 띄우는 함수를 실행하도록 코드를 구현하는 것)
*Non-blocking I/O와 Event-Driven은 서로 상호보완적인 관계에 있다. 결국 둘 다 프로그램의 효율성을 높이고 성능을 더욱 향상시키게 해주는 역할을 한다.
*앞서 설명한대로 위의 모든 특징은 JS에도 적용되는 특징이다. 고로 Node.js가 지닌 특징은 JS가 가진 특징이기도 하다.
JS의 동작원리
JS와 Node.js의 동작원리는 비슷하다. 그렇기 때문에 JS의 동작원리에 대해 이해를 한 뒤 둘의 차이점을 이해한다면 더욱 수월하게 동작원리에 대해 알 수 있게 될 것이다.
-HTML, CSS, JS가 담긴 파일들이 있다고 가정해보자. 어떤 웹사이트에 접속을 하는 순간 모든 코드들은 일단 메모리에 저장이 되고 HTML, CSS, JS의 코드를 브라우저가 해석하게 된다. 이 때 브라우저가 해석하면서 HTML은 Dom을, CSS는 cssom을 구성하여 render tree를 만들게 된다.
-그리고 JS의 경우에는 브라우저가 해석할 때 먼저 ‘정적인 코드’는 HTML, CSS를 렌더링 할 때 함께 렌더링을 진행하게 된다.
그러나 ‘동적인 코드’의 경우에는 다르다. 예를 들어 버튼을 눌렀을 때 경고창이 뜨도록 구현한 코드(click 이벤트)가 있을 때 이 코드는 일단 메모리에 저장이 되어 있다가 버튼을 누르는 순간 브라우저가 JS의 엔진을 통해 single thread(main thread)를 실행하게 되고 해당 함수와 함수와 관련된 지역변수 같은 것들이 담겨있는 ‘함수 실행 컨텍스트’가 스택에 올려지면서 함수가 실행이 되게 된다.
정적인 코드의 경우에는 브라우저가 곧바로 렌더링을 하기 때문에 간단하다. 그에 비해 '동적인 코드'의 경우에는 어떤 과정을 거쳐서 사용자에게 최종적으로 보여지는지에 대해서 이해 할 필요가 있다.
먼저 JS의 엔진은 엔진 내부에 있는 '어플리케이션 파트'와 엔진 외부에 있는 'WEB APIs 파트'로 나눌 수 있다.
-어플리케이션 파트에는 힙(Heap)과 스택(Stack)이 있다.(동기적인 코드를 담당. 원칙적으로 스레드 1개)
-WEB APIs 파트에는 큐(Queue)가 존재하고 event-loop가 있다.(비동기적인 코드를 담당. 멀티 스레드)
특정 이벤트가 발생했을 때 이벤트에 등록한 함수가 실행이 되는데 그 함수가 실행되면 해당 함수, 그 함수와 관련된 변수들, 비동기적인 코드들이 스택, 힙, 큐 라는 곳에 각각 들어가게 되고 JS의 동작원리에 따라 해당 코드들이 적절하게 상호작용을 이루면서 실행이 되어 최종적으로 사용자에게 보여지게 된다. 그렇다면 스택과 힙과 큐에는 어떤 코드들이 들어가게 될까?
스택과 힙과 큐에는 아래와 같은 코드들이 들어가게 된다.
1. 스택(Stack): 함수 호출과 변수들이 저장되는 곳이다. 기본 데이터 타입(숫자, 문자열, 불리언 등)은 값이 직접 스택에 저장되고, 복잡한 데이터 타입(객체, 배열 등)의 경우 스택에 참조 값(ref)이 저장된다. 함수 호출이 발생하면, 호출된 함수의 지역 변수와 매개 변수가 스택에 저장되며, 함수가 종료되면 스택에서 제거된다. 그리고 single thread이기 때문에 스택은 1개만 존재한다.
2. 힙(Heap): 힙은 복잡한 데이터 타입(객체, 배열 등)의 실제 데이터를 저장하는 공간이다. 스택에 저장된 참조 값은 이 힙 영역의 데이터를 가리킨다. 힙은 구조적으로 정렬되지 않은 메모리 영역으로, 데이터의 크기가 가변적이거나 생명 주기가 긴 경우 사용된다.
3. 큐(Queue): 큐는 FIFO(First-In, First-Out) 구조로, 비동기 작업(예: setTimeout, AJAX 요청 등)의 콜백 함수가 대기하는 곳이다. 이벤트 루프는 스택이 비워질 때마다 큐에서 콜백 함수를 가져와 실행한다.
(큐는 ‘대기열’ 이라고도 부르고 ‘태스크 큐’라고도 부른다.)
*위 내용을 바탕으로 코드가 실행되는 과정을 설명해보려고 한다.
1.HTML, CSS, JS의 모든 코드들은 메모리에 저장이 되고, 브라우저는 이를 바탕으로 HTML ,CSS, 정적인 JS 코드를 먼저 해석을 한 뒤 사용자에게 보여주게 된다.
2.사용자가 어떠한 이벤트를 발생시키면 이벤트에 의해 실행되는 동적인 함수가 ‘동기적’인 함수라면 스택과 힙에 코드가 들어가게 되고 ‘비동기적’인 함수라면 JS 엔진 외부에서 멀티 스레드에 의해 함수를 처리한다.
3.그 후 JS 엔진 외부에서 처리가 완료된 비동기 함수는 큐로 이동을 하게 되고 동시에 스택에 있는 함수가 순차적으로 실행이 되면서 스택이 비워지게 된다.
4.스택이 비워지게 되면 그 때 event loop는 큐에 있던 비동기 함수를 먼저 들어온 순서대로 스택에 올려주고 스택에서 실행이 되어 사용자에게 보여지게 된다.
(큐에 있는 비동기 코드는 스택이 비워졌을 때 비로소 올려질 수 있다는걸 알아야 한다.)
*이 모든 과정은 main thread에 의해 진행이 된다. 스택과 힙에 코드를 올리고 실행시키고, 비동기 코드를 엔진 외부로 보내고 event loop를 동작하게 하는 것도 main thread 이다. 그리고 엔진 외부에 있는 멀티 스레드는 비동기 코드를 실행시키는 역할을 한다.
*부연 설명
-스택과 힙에 정확히 어떤 데이터가 들어가게 되는지에 대해서 설명을 해보자면, 변수에 지정된 타입이 가벼운 데이터(숫자, 문자열, 불리언 등)면 스택에 저장이 되는 것이고 무거운 데이터 타입인 데이터는 ref만 스택에 저장이 될 뿐 실질적인 데이터는 힙에 저장이 된다.
(예를 들어 객체와 배열이 있을 때 이들의 ref는 스택에 저장이 되는 것이고, 객체와 배열에 실제 들어 있는 데이터의 경우에는 힙에 저장이 되는 것이다.)
-함수가 호출이 되면 해당 함수가 스택에 올려진다고 했는데, 더 정확하게 얘기하면 '함수 실행 컨텍스트'가 스택에 올려지는 것이다. 이는 함수가 호출되었을 때, 그 함수에 필요한 변수들을 한 곳에 모아서 스코프와 독립성을 보장하기 위해 존재한다. 실행 컨텍스트는 함수 실행에 필요한 모든 정보를 포함하며, 스코프 체인을 관리하여 변수와 함수의 접근 권한 및 가시성을 제어한다. 이를 통해 JavaScript는 함수 실행의 흐름을 제어하고, 각 함수의 독립된 실행 환경을 보장할 수 있다.
⇒ 즉 함수가 호출 되었을 때 해당 함수와, 그 함수에 필요한 변수들을 한 곳에 모아서 '함수 실행 컨텍스트'가 된다. 이는 스코프나 독립성을 보장하기 위해 존재한다.
(함수 실행이 완료되면 해당 함수의 실행 컨텍스트는 스택에서 지워지게 된다.)
-비동기 함수에 엔진 외부에서 처리가 된다고 했는데, 이를 예시를 통해 자세히 설명을 해보자면 setTimeout 함수를 사용했을 때 3초 라는 조건을 설정했다면 엔진 외부에서 멀티 스레드에 의해 3초가 지나길 기다린 뒤 이를 큐에 이동을 시키고 이후 스택이 비워져 있을 때 메인 스레드에 의해 이벤트 루프가 스택으로 올려주고 스택에서 실행이 되게 된다.
*여기까지 설명한 내용인 Node.js의 특징, JS의 동작원리를 이렇게 나눠서 설명을 하긴 했지만 Node.js와 JS 둘 다 해당되는 이야기이다. 즉 JS와 Node.js는 몇가지만 빼놓고 보면 아예 동일한 특징과 동일한 동작원리를 지니고 있다. 이제 아래에서 Node.js를 기준으로 JS와 차이점이 무엇이 있는지를 설명하려고 한다.
=> 즉, 위의 모든 내용은 JS와 Node.js의 공통점에 대한 이야기이고, 아래에 설명할 Node.js가 가진 JS와의 차이점을 통해 전체적인 내용을 쉽게 이해할 수 있게 될 것이다.
Node.js의 동작원리
Node.js와 JS의 차이점
1.엔진 외부의 이름의 차이
Node.js에는 엔진 내부에 있는 ‘어플리케이션 파트’ 와 엔진 외부에 있는 ‘Node APIs 파트’가 나눠져 있다.
-어플리케이션 파트에는 힙(Heap)과 스택(Stack)이 있다.(동기적인 코드를 담당하는 곳)
-Node APIs 파트에는 큐(Queue)가 존재하고 event-loop가 있다.(비동기적인 코드를 담당하는 곳)
=> 비동기적인 코드를 담당하는 곳의 이름이 다를 뿐 구조는 JS와 동일하다.
2. JS의 경우 브라우저에서 제공하는 API를 사용할 수 있지만, Node.js는 사용이 불가하다. 대신 Node.js는 모듈을 통해 수 많은 패키지를 사용할 수 있다.
3. JavaScript는 브라우저에서 실행되는 클라이언트 사이드 스크립트 언어로, 주로 웹 페이지에서 동적인 기능을 구현하는 데 사용된다.
반면에 Node.js는 서버 사이드 자바스크립트 런타임 환경으로, 서버 측에서 JavaScript 코드를 실행하고 다양한 기능을 제공한다. Node.js는 V8 JavaScript 엔진을 기반으로 하며, 파일 시스템, 네트워크, 암호화 등의 기능을 제공하여 서버 측에서 웹 어플리케이션을 개발할 수 있도록 도와준다.
⇒ 즉 JS는 브라우저에서 실행이 되는거지만, Node.js는 브라우저 밖에서 실행이 된다는 차이가 있다. 이러한 점 때문에 JS는 클라이언트를 구성하는데 좋고 Node.js는 서버를 구성하는데 좋다.
추가적인 내용
*JS와 Node.js는 Single thread이기 때문에 비동기적인 함수도 결국 stack에서 처리가 된다. 고로 아무리 비동기 함수를 여러개 만들 수 있다고 해도 이를 남발하게 되면 결국은 stack이 정체되는 현상이 생기게 된다.
*또한 반복문을 많이 사용하면 stack은 하나이기에 속도 또한 느려지게 된다. 그리고 복잡하고 어려운 작업을 시키면 시간이 늘어난다.
*만약 10초나 걸리는 코드를 짰다면 Stack에서 기존 코드들을 연산하느라 스택에서 아직 못 빠져 나가고 있으면 큐에 있는 애들이 올라오질 못한다. 그럼 사용자는 웹페이지에 있는 로그인 버튼 같은걸 눌러도 아직 큐에서 Stack으로 버튼 이벤트가 못 넘어왔기에 실행이 되질 않게 된다.
⇒ Stack과 Queue를 바쁘게 하면 그 만큼 웹페이지가 느려진다.
*Node.js를 사용해서 서버를 만들 때는 I/O 적인 부분에서는 엄청나게 좋은 점들을 많이 가지고 있다. 하지만 무거운 연산 등을 하기에는 안 좋기 때문에 CPU적인 부분에서는 좋지 않다. 고로 가벼운 프로젝트를 만들 때 Node.js는 탁월한 선택이지만, 복잡하고 무거운 프로젝트를 만들 때는 적합한 선택은 아닐 수 있다.
*자바스크립트에서는 가비지 컬렉션(Garbage Collection)이라는 메모리 관리 기법을 사용한다. 가비지 컬렉션은 더 이상 필요하지 않은 메모리를 해제하는 프로세스이다. 이 과정에서, 가비지 컬렉터가 힙에서 더 이상 사용되지 않는 데이터를 찾아서 메모리를 해제한다. 이를 통해, 메모리 누수(memory leak)를 방지하고 메모리 사용을 최적화할 수 있다.
*JS는 매우 유연한 언어이다. 만약 a 라는 변수를 설정하지도 않았는데 a = 6; 이라고 할당시키면 이를 a = 6이라고 인지를 해버리기 때문에 위험한 상황이 생길수도 있다. 아래의 예시를 통해 확인해보자.
function exampleFunction() {
a = 10; // 선언되지 않은 변수에 값을 할당
console.log(a); // 콘솔을 통해 실제로 출력되는지를 확인
}
exampleFunction(); // 에러 없이 실행되고, 10을 출력한다.
-위의 예시를 보면 let 또는 const를 사용해서 변수를 선언한게 아니라, 아예 A = B 라고 할당을 시켜버린 케이스이다.
-이런 식으로 변수를 설정하지 않고 할당을 시켜버리면, 이 값은 '전역 변수'로 생성이 된다. 고로 프로그래밍을 해나가다가 기존에 변수를 선언하지 않고 할당한 a에 대해서 const a = 20으로 변수 선언을 한 경우, 기존에 a는 10으로 아예 할당이 되었기 때문에 a를 20으로 선언하려고 하더라도 계속 10으로 남아있게 된다. 이렇게 되면 디버깅 또한 매우 어려워진다. 코드가 길어지고 복잡해졌을 때 이 오류를 찾아내기란 쉽지 않을 것이다. 아래의 예시를 보자.
a = 10; // 선언되지 않은 변수에 값을 할당
function exampleFunction() {
const a = 20;
console.log(a); // 콘솔을 통해 어떤 값이 출력되는지를 확인
}
exampleFunction(); // 에러 없이 실행되고, 10을 출력한다.
-이렇게 변수 없이 10을 할당한 a는 향후 a를 변수로 20을 선언하더라도 계속 10을 출력하게 된다.
위와 같은 상황을 방지하기 위해 애초에 코딩을 할 때 변수를 항상 선언하는 습관을 가지는 것이 좋다. 또는 use strict를 써주면 이런 것들을 방지해준다.
'use strict';
a = 6;
function exampleFunction() {
let a = 10;
console.log(a);
}
exampleFunction();
// ReferenceError: a is not defined
// 변수 선언이 없기 때문에 위와 같은 에러가 발생한다.
-위와 같이 최상단에 use strict를 써주면 된다. 이렇게 되면 기존과는 달리 에러 메시지를 보내주기 때문에 곧바로 오류를 찾아낼 수 있다.
'기술 노트' 카테고리의 다른 글
서버 트래픽 과부하에 대한 예방과 대처 방법 (0) | 2023.05.07 |
---|---|
쿠키와 세션, OAuth, JWT에 대하여 (0) | 2023.04.29 |
DB Index란? (0) | 2023.03.28 |
컴퓨터의 구조와 OS의 관계 (0) | 2023.03.21 |
defer과 async를 통해 HTML과 JS를 연결하기. (0) | 2023.03.19 |