Node.js intro
Node.js 알고 쓰자
JavaScript 기초 문법
1. 변수 선언
JavaScript에서는 var
, let
, const
를 통해 변수를 선언한다.
var x = 10;
let y = 20;
const z = 30;
var
는 함수 스코프를 가지며, 호이스팅된다.let
,const
는 블록 스코프를 가지며, Temporal Dead Zone에 의해 선언 전 접근 시 ReferenceError가 발생한다.const
는 재할당이 불가능하나, 객체 내부 프로퍼티 변경은 가능하다.
Temporal Dead Zone (TDZ): let이나 const로 선언된 변수가 선언되기 전까지 접근이 불가능한 구간
호이스팅: 자바스크립트는 변수를 먼저 “호이스팅(Hoisting, 끌어올림)”한다. 변수 선언과 함수 선언이 해당 코드 블록의 최상단으로 끌어올려지는 현상을 말한다. 즉, 코드 실행 전에 선언이 자동으로 끌어올려지는 것이다.
하지만, let과 const는 초기화 이전까지는 사용할 수 없는 “죽은 시간대”에 놓인다.
var는 호이스팅된 후 undefined로 초기화되지만, let은 선언 시점까지 접근이 불가능한 “죽은 시간대(Temporal Dead Zone)”에 존재한다. 이 시점까지는 변수가 아예 존재하지 않는 것처럼 취급된다.
const obj = { a: 1 };
obj.a = 2; // 가능
obj = { b: 3 }; // 에러
2. 데이터 타입
원시 타입 (Primitive Types)
자바스크립트의 원시 타입은 내부적으로 결정된다. 원시 타입은 불변(immutable) 값들을 가지며, 자바스크립트 엔진에 의해 특정한 방식으로 처리된다.
-
자바스크립트의 원시 타입은 모두 값 자체를 저장하고, 참조가 아닌 값을 직접 다룬다.
그러므로 값의 일부를 바꾸는 문법은 동작하지 않는다. let str = ‘hello’; str[0] = ‘H’; // 변경되지 않음 console.log(str); // ‘hello’
-
내부 처리
원시 타입 값들은 자바스크립트 엔진에 의해 특정한 방식으로 처리된다. 예를 들어, string은 유니코드 값으로 처리되고, number는 IEEE 754 표준에 맞춰 처리된다. 각 원시 타입은 내부적으로 특정한 형식과 규칙에 따라 처리된다
-
원시 타입은 복사할 때 값 자체가 복사되며, 객체는 참조를 복사한다. 이 차이는 원시 타입이 값의 불변성과 독립성을 보장하는 데 중요한 역할을 한다.
string
number
boolean
null
undefined
symbol
bigint
let s = "hello";
let n = 123;
let b = true;
let u = undefined;
let nn = null;
참조 타입 (Reference Types)
- 객체(Object), 배열(Array), 함수(Function)
3. 조건문
let x = 5;
if (x > 0) {
console.log("양수");
} else if (x < 0) {
console.log("음수");
} else {
console.log("0");
}
삼항 연산자도 사용 가능하다.
let result = x > 0 ? "positive" : "non-positive";
4. 반복문
// while
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
// for
for (let i = 0; i < 5; i++) {
console.log(i);
}
// for..of (iterable 객체)
const arr = [1, 2, 3];
for (let v of arr) {
console.log(v);
}
// for..in (객체 속성)
const obj = { a: 1, b: 2 };
for (let k in obj) {
console.log(k, obj[k]);
}
5. 리스트 (배열)
배열은 동적이고, 다양한 타입의 값을 동시에 담을 수 있다.
let list = [1, "two", true];
list.push(4); // 추가
list.pop(); // 마지막 요소 제거
list[1]; // 접근
list.length; // 길이
고차 함수도 풍부하게 제공된다.
list.map(x => x + "!");
list.filter(x => typeof x === "number");
list.reduce((a, b) => a + b, 0);
6. 함수 선언
// 선언식
function add(a, b) {
return a + b;
}
// 표현식
const sub = function (a, b) {
return a - b;
};
// 화살표 함수
const mul = (a, b) => a * b;
기본 인자를 지정할 수 있다.
function greet(name = "Guest") {
return "Hello, " + name;
}
7. 객체 선언 및 조작
const user = {
name: "Alice",
age: 25,
greet() {
return `Hi, I'm ${this.name}`;
}
};
user.name; // 접근
user["age"]; // 접근
user.job = "dev"; // 추가
delete user.age; // 삭제
8. 클래스
ES6부터 class 문법이 도입되었다. 실제로는 프로토타입 기반 객체지향이다.
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
const p = new Person("Bob");
p.greet();
상속은 extends
, 부모 클래스 호출은 super()
를 사용한다.
9. 에러 핸들링
JavaScript는 예외 발생 시 throw
, try-catch
를 사용한다.
try {
throw new Error("Something went wrong");
} catch (e) {
console.error(e.message);
} finally {
console.log("항상 실행");
}
자주 쓰이는 에러 객체에는 Error
, TypeError
, ReferenceError
, SyntaxError
등이 있다.
10. 비동기 처리 기본
// setTimeout: 지연 실행
setTimeout(() => {
console.log("1초 후 실행");
}, 1000);
// Promise
const p = new Promise((resolve, reject) => {
resolve("완료");
});
p.then(res => console.log(res)).catch(err => console.error(err));
async/await
는 비동기 코드를 동기식처럼 작성할 수 있게 해준다.
‘Promise’ 기반의 비동기 코드를 가독성 높게 작성할 수 있도록 도와준다.
async function main() {
try {
const result = await p;
console.log(result);
} catch (err) {
console.error(err);
}
}
11. 모듈
// a.js
export const pi = 3.14;
// b.js
import { pi } from './a.js';
Node.js에서는 CommonJS 방식이 기본이다.
// CommonJS
const fs = require('fs');
JavaScript 변수 선언 키워드: var
, let
, const
1. var
- ES5 이전의 기본 변수 선언 방식
특징
- 함수 스코프(Function Scope)
var
로 선언된 변수는 가장 가까운 함수 블록을 기준으로 스코프가 결정된다. 이는{}
블록에 국한되지 않는다.
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10
}
- 호이스팅(Hoisting)
var
로 선언한 변수는 선언이 스코프의 최상단으로 끌어올려진다. 단, 초기화는 호이스팅되지 않는다.
console.log(a); // undefined
var a = 10;
- 중복 선언 허용
같은 스코프 내에서var
는 중복 선언이 가능하다.
var x = 1;
var x = 2; // 가능
주의점
- 블록 레벨 스코프가 없기 때문에, 의도하지 않은 변수 오염이나 덮어쓰기가 발생할 수 있다.
var
는 ES6 이후에는 거의 사용되지 않으며,let
또는const
가 권장된다.
2. let
- ES6에서 도입된 블록 스코프 변수 선언 방식
특징
- 블록 스코프(Block Scope)
let
은{}
로 감싸진 블록 단위로 스코프가 형성된다.
{
let y = 20;
}
// console.log(y); // ReferenceError
- TDZ(Temporal Dead Zone)
let
변수는 선언 전에 접근하면 ReferenceError가 발생한다. 선언 전에 변수에 접근하는 것이 차단된다.
console.log(b); // ReferenceError
let b = 5;
- 중복 선언 불가
동일한 스코프 내에서 중복 선언은 허용되지 않는다.
let c = 1;
// let c = 2; // SyntaxError
- 재할당 가능
let d = 3;
d = 4; // 가능
주의점
- 호이스팅은 되지만, TDZ 때문에 실제로는 호이스팅된 시점 이전에 접근할 수 없다.
for
루프 내부에서 비동기 함수와 함께 사용할 때 유용하다. (var
는 클로저 문제를 일으킴)
3. const
- 상수(immutable binding)를 선언하는 방식
특징
- 블록 스코프
- TDZ 적용
- 재할당 불가
const pi = 3.14;
// pi = 3.14159; // TypeError
- 중복 선언 불가
- 선언 시 반드시 초기화되어야 한다.
// const x; // SyntaxError
주의점
const
는 바인딩 자체가 불변이라는 의미이며, 객체의 내부 프로퍼티는 변할 수 있다.
const obj = { a: 1 };
obj.a = 2; // 가능
obj = {}; // TypeError
- 배열도 마찬가지로 요소의 추가, 삭제는 가능하다.
const arr = [1, 2];
arr.push(3); // 가능
항목 | var |
let |
const |
---|---|---|---|
스코프 | 함수(Function) | 블록(Block) | 블록(Block) |
호이스팅 | O (undefined로 초기화) | O (TDZ 발생) | O (TDZ 발생) |
중복 선언 | O | X | X |
재할당 | O | O | X |
초기화 필요 여부 | X | X | O |
사용 권장 여부 | ❌ | ✅ | ✅ (불변일 경우) |
Node.js란
Node.js는 브라우저 외부에서 JavaScript를 실행할 수 있는 런타임 환경(runtime environment)이다.
이는 구글 크롬에서 사용하는 V8 JavaScript 엔진 위에 구축되었으며, 이벤트 기반(Event-driven), 논블로킹(non-blocking) I/O 모델을 채택하여 고성능 네트워크 애플리케이션에 적합하다.
서버 측에서 수많은 요청을 비동기 이벤트 큐로 처리할 수 있어 고성능 구현이 가능하다.
✅ 이벤트 기반(Event-driven) 자바스크립트는 이벤트가 발생할 때 실행할 콜백 함수를 등록해두고, 그 이벤트가 발생하면 해당 함수가 자동으로 실행되는 방식으로 동작한다. 대표적으로 클릭, 타이머, 서버 응답 등이 이벤트이다.
✅ 논블로킹(non-blocking) I/O I/O 작업(예: 파일 읽기, 네트워크 요청 등)이 끝날 때까지 기다리지 않고,다음 코드로 넘어간다. 작업이 끝나면 콜백 함수나 Promise를 통해 결과를 처리한다.
논블로킹과 멀티스레드 비동기처리 차이
결론부터 말하면 겉보기엔 비슷하지만, 작동 방식은 다르다.
- 멀티스레드 비동기 vs 논블로킹 I/O (싱글스레드 기반)
항목 | 멀티스레드 비동기 | 논블로킹 I/O (ex. Node.js) |
---|---|---|
방식 | 작업마다 새로운 스레드 생성 | 싱글 스레드 + 이벤트 루프 사용 |
I/O 처리 | 스레드가 병렬로 I/O 수행 | 커널이 I/O 처리 후 이벤트로 알려줌 |
자원 사용 | 스레드 수에 따라 메모리 소비 | 상대적으로 가볍고 효율적 |
문맥 전환 비용 | 높음 (스레드 간 전환 필요) | 거의 없음 |
병렬성 | 하드웨어 코어 수만큼 병렬 | 진정한 병렬은 아님 (논리적 동시성) |
- 멀티스레드 방식 (Java, C++ 등)
std::thread t([]() {
longIOOperation();
});
t.join();
- I/O 작업마다 스레드를 하나 만들어서 비동기 수행.
- 동시에 여러 작업이 진짜 병렬로 돌아간다.
- 하지만 스레드가 많아지면 메모리/CPU 오버헤드가 커진다.
- 논블로킹 방식 (Node.js)
fs.readFile("file.txt", (err, data) => {
console.log("파일 읽기 완료");
});
console.log("다른 코드 실행");
- 싱글 스레드에서 논블로킹 I/O 요청만 하고, 바로 다음 작업을 실행.
- 파일이 다 읽히면 이벤트 루프가 콜백을 실행한다.
- 실제 I/O는 OS 커널이 처리하고, 결과만 나중에 콜백으로 전달된다.
- 결론
- 멀티스레딩은 “진짜 병렬”이며, 각 작업마다 스레드가 분기된다.
- 논블로킹은 “병렬처럼 보이는 순차 처리”이며, 싱글스레드에서 이벤트 루프 기반으로 비동기를 처리한다.
- 결과적으로 비슷하게 “비동기 처리가 가능”하지만, 방식과 장단점은 다르다.
✍️ 즉, 논블로킹은 멀티스레드로 분기하는 것이 아니라
이벤트 루프와 OS의 비동기 지원 기능을 활용한 싱글스레드 모델이다.
논블로킹과 멀티스레드 비동기처리의 I/O 처리 차이
스레드를 분기(예: 각 I/O 작업마다 새 스레드 생성)해도 메인 스레드는 블로킹되지 않으니 논블로킹처럼 보이는 건 맞다. 하지만 libuv + 이벤트 루프 방식과는 철학도 다르고, 성능·안정성에서도 큰 차이가 있다.
- 🔍 I/O마다 스레드를 분기하는 방식의 한계
(예: Java의 요청당 스레드 방식)
- ❗ **스레드는 무거운 자원이다**
- 스레드 하나당 수백 KB~1MB의 메모리 스택이 필요하다.
- 1만 개 요청이 동시에 오면 1만 개 스레드 → 메모리 폭발.
- ❗ **컨텍스트 스위칭 비용이 크다**
- 스레드가 많아질수록 CPU는 문맥 전환(context switch)에 더 많은 비용을 쓴다.
- 성능 저하 + CPU 캐시 효율도 떨어짐.
- ❗ **락과 동기화 문제**
- 스레드 간 자원 공유가 발생하면 락, 뮤텍스 등을 써야 한다.
- 코드 복잡도 증가 + 병목 현상 발생.
- ✅ libuv 기반 이벤트 루프 방식의 장점
- 🌱 **스레드 하나로 수천~수만 개 요청 처리 가능**
- 이벤트 루프는 이벤트를 큐에 넣고 비동기로 처리한다.
- 실제 I/O는 libuv 스레드 풀이나 OS 이벤트 시스템이 처리한다.
- ⚡ **컨텍스트 스위칭 비용이 거의 없다**
- 대부분의 실행이 단일 스레드에서 이뤄지기 때문에 전환 비용이 적다.
- CPU 캐시도 잘 활용할 수 있다.
- 🔒 **락이 필요 없다**
- JS는 단일 스레드에서 실행되기 때문에 동시성 문제 거의 없다.
- 코드 흐름이 단순하고 예측 가능하다.
3. 🧠 **요약 비교표**
| 항목 | 스레드 분기 방식 | 이벤트 루프 방식 (Node.js) |
|------|------------------|-----------------------------|
| 동시성 처리 방식 | 요청마다 스레드 생성 | 이벤트 큐 + 비동기 콜백 |
| 확장성 | 제한적 (수천 개 수준) | 매우 높음 (수만 개 가능) |
| 메모리 효율 | 낮음 | 높음 |
| 복잡성 | 락과 동기화 필요 | 단순한 상태 관리 |
| CPU 효율 | 낮음 (스위칭 비용 큼) | 높음 |
| 예시 | Java, Apache 등 | Node.js, Nginx, Netty 등 |
4. 🧠 **결론**
스레드 분기 방식은 구조가 직관적이지만, 메모리나 CPU 자원 소모가 크고 락 등의 동기화 문제가 발생하기 쉽다.
반면, 이벤트 루프 기반 방식은 구조가 살짝 더 복잡하지만, 적은 자원으로도 훨씬 많은 요청을 효율적으로 처리할 수 있다.
특히 Node.js처럼 이벤트 기반 구조를 채택한 시스템은 **실시간성**이 중요한 웹소켓, 스트리밍, API 서버 등에 잘 어울린다.
필요하면 흐름도나 실제 예제도 만들어줄 수 있다.
비동기 좀만 더 Deep 하게
- 왜 비동기는 더 “빠른가”?
비동기가 더 "빠르다"라고 표현되는 이유는 리소스를 더 효율적으로 활용하기 때문이다. 정확히 말하자면, 비동기는 대기 시간을 최소화하고, 자원을 유휴 상태로 두지 않기 때문에 더 많은 작업을 동시에 처리할 수 있다.
프로세스 status를 공부하다 보면 I/O인터럽트 등의 발생으로 프로세스가 ready 상태(=**대기 상태, Blocking**)에 돌입한다.
이것은 CPU 자원이 쉬고있는 구간을 의미하게 된다.
비동기 방식은 이 쉬는 구간에 다른 프로세스에게 CPU를 할당하고, ready상태의 프로세스는 I/O 작업이 끝났다는 이벤트가 발생하고 나서야 수행된다.
이것은 멀티스레드로 I/O작업을 분기하는 방식에서도 비슷하게 구현할수 있으나 상기한 멀티스레드의 메모리 문제, 구현 복잡도, 컨텍스트 스위칭으로 인한 오버헤드 등의 이유로 메모리에 상주하는 메시징 큐나, 논블로킹I/O 기반의 구조가 더 고성능으로 이어진다.
1. **메인 스레드의 유휴 상태 최소화**
동기 처리 방식에서는 각 I/O 작업을 할 때마다 메인 스레드가 결과를 기다리면서 멈춘다.
예를 들어, 파일을 읽을 때 메인 스레드는 그 작업이 끝날 때까지 기다린다.
반면 비동기 처리에서는 메인 스레드가 I/O 작업을 요청한 후 기다리지 않고 다른 작업을 처리한다.
작업이 완료되면 콜백을 통해 결과를 처리하기 때문에, 메인 스레드는 I/O가 완료될 때까지 기다릴 필요가 없다.
→ 결과적으로 더 많은 일을 동시에 처리할 수 있고, 자원을 낭비하지 않기 때문에 더 효율적이다.
2. **I/O 작업을 블로킹하지 않는다**
동기 처리에서는 I/O가 끝날 때까지 기다리므로, 그동안 메인 스레드는 다른 작업을 할 수 없다.
예를 들어, 디스크 읽기, 네트워크 요청 등에서 메인 스레드는 결과를 기다리며 멈춰 있기 때문에, CPU는 다른 작업을 하지 못한다.
비동기 처리에서는 I/O가 백그라운드에서 처리되는 동안, 메인 스레드는 다른 요청을 처리할 수 있다.
그 결과 전체 시스템의 처리량이 높아지게 된다.
3. **병렬 처리 및 비동기 작업 큐**
비동기 방식에서는 I/O 작업이 여러 개 동시에 처리될 수 있다.
예를 들어, 네트워크 요청 5개가 동시에 일어나면, 동기 방식에서는 하나씩 끝날 때까지 기다려야 하지만, 비동기 방식에서는 5개의 요청을 동시에 던져놓고, 각 요청이 끝난 순서대로 결과를 처리할 수 있다.
이를 통해 메인 스레드는 유휴 상태 없이 계속 다른 작업을 할 수 있다.
4. **자원 관리의 효율성**
비동기 방식은 스레드를 하나만 사용하기 때문에, 스레드의 생성과 종료에 드는 비용이 발생하지 않는다.
대신 여러 I/O 작업을 단일 스레드에서 효율적으로 처리한다.
스레드 풀을 사용하지 않기 때문에, 스레드를 관리하는 오버헤드가 적고, 컨텍스트 스위칭에 드는 비용도 최소화된다.
동기 방식에서는 많은 스레드를 생성하여 관리해야 하기 때문에 메모리와 CPU 자원 소모가 커지고, 성능에 영향을 줄 수 있다.
-
Node의 비동기 처리방식(I/O에 있어서) 🔁 요청 순서 vs 응답 순서
-
Node.js의 이벤트 루프 기반 비동기 모델에서는:
-
요청 순서대로 I/O 작업을 던진다. (백그라운드로)
-
하지만 각 I/O 작업의 실행 시간은 다를 수 있다.
-
작업이 먼저 끝난 순서대로 콜백이 이벤트 큐에 들어간다.
-
이벤트 루프는 큐에서 순차적으로 콜백을 실행한다.
→ 결과적으로, 응답은 요청 순서와 다르게 도착할 수 있다. 이게 바로 비동기 처리의 본질이다.
-
즉, **큐에 들어가는 것은 실제 I/O 작업 그 자체가 아니라, I/O 작업이 끝났다는 "이벤트"나 "콜백"**이다.
CPU 바운드 작업에서 Node.js의 논블로킹 방식이 불리한 이유
CPU 바운드 작업에서 Node.js의 논블로킹 방식이 불리한 이유는 CPU 자원을 다루는 방식에 기인한다.
멀티스레드 방식은 여러 CPU 코어를 활용하여 병렬로 CPU 바운드 작업을 처리할 수 있어서, 계산을 여러 스레드로 나누어 동시 처리가 가능하다. 이렇게 하면 CPU 자원을 효율적으로 사용할 수 있다.
반면, Node.js의 단일 스레드 논블로킹 방식은 하나의 CPU 코어에서만 작업을 처리하므로, CPU 바운드 작업을 처리하는 동안 이벤트 루프가 멈추거나 차단된다. 이는 다른 작업을 처리할 수 없게 되어 성능이 저하될 수 있다.
- Node.js의 논블로킹 방식이란?
Node.js는 **이벤트 루프**를 기반으로 동작하고, 비동기 I/O 작업을 처리할 때 **단일 스레드**로 작업을 처리한다. 이때, I/O 작업은 논블로킹으로 실행되지만, **CPU 바운드 작업**(계산이 많이 필요한 작업)은 논블로킹 방식으로 잘 처리되지 않는다.
- CPU 바운드 작업에서 발생하는 문제
CPU 바운드 작업은 **계산을 많이 요구**하는 작업이다. 예를 들어, 복잡한 수학 연산, 데이터 처리, 파일 압축 등은 모두 CPU를 집중적으로 사용한다.
- **단일 스레드**인 Node.js는 CPU 바운드 작업을 처리할 때 **이벤트 루프가 막히게 된다.**
- **CPU 작업이 끝날 때까지** 이벤트 루프는 다른 요청을 처리할 수 없다. 즉, 계산을 하는 동안 다른 요청들이 **대기 상태**로 쌓이게 된다.
- 멀티스레드 방식의 장점
반면, **멀티스레드 방식**에서는 **각 CPU 바운드 작업을 별도의 스레드**에서 처리할 수 있다. 여러 스레드를 병렬로 실행함으로써, **CPU를 효율적으로 분배**하고 **이벤트 루프를 차단하지 않게** 할 수 있다.
예를 들어, Java나 C#과 같은 멀티스레드 환경에서는 각각의 CPU 바운드 작업을 다른 스레드에서 처리할 수 있기 때문에, 이벤트 루프가 차단되지 않고 **다양한 작업을 동시에 실행**할 수 있다.
- Node.js에서 CPU 바운드 작업 처리 방법
Node.js가 CPU 바운드 작업을 처리하는 방식에는 **멀티스레딩**을 사용하지 않지만, **클러스터링**이나 **웹 워커** 등을 활용하여 멀티코어 시스템을 활용할 수 있다.
- **클러스터링**: Node.js의 `cluster` 모듈을 사용하면 여러 프로세스를 생성해 CPU 바운드 작업을 분산 처리할 수 있다.
- **웹 워커**: `worker_threads` 모듈을 사용하면 별도의 스레드를 생성하여 CPU 바운드 작업을 처리할 수 있다.
하지만 이런 방식도 **추가적인 관리 비용**과 **복잡성**을 수반하므로, 기본적으로는 **Node.js가 CPU 바운드 작업에 불리**한 구조인 것이다.
-
결론
- I/O 작업에서는 비동기식 논블로킹 방식이 매우 유리하지만,
- CPU 바운드 작업에서는 계산이 오래 걸리기 때문에 이벤트 루프가 차단되어 다른 작업을 처리할 수 없는 문제가 발생한다. 이 때문에 멀티스레드 방식이 더 효율적이다.
CPU 바운드 작업에 비동기 방식이 불리한 이유는 단일 스레드가 계산을 처리하는 동안 이벤트 루프가 멈추기 때문이며, 이로 인해 다른 요청들이 대기하게 되는 문제가 발생한다.
근데 비동기 처리하고 하니까 용어가 참 헷갈린다. 사실 멀티 스레드도 비동기로 작동하지 않는다. 앞으로 비동기 방식이라는 언어는 단일 스레드만 보았을떄 I/O작업에 대하여 비동기 이다. 라고 이해하면 될것같다.
1. 구성 요소
1.1. V8 엔진
- 구글에서 개발한 C++ 기반의 JavaScript 엔진
- JavaScript를 머신 코드로 빠르게 컴파일하여 실행
- Node.js는 이 엔진을 사용해 JavaScript 코드를 실행한다
1.2. libuv
- 비동기 I/O 처리 및 이벤트 루프를 담당하는 라이브러리
- 파일 시스템, TCP/UDP, DNS 등의 비동기 API 제공
- Unix, Windows 등 멀티 플랫폼 지원
1.3. Node.js 자체 API
- 브라우저와는 달리 DOM이나 window 객체는 없음
- 대신
fs
,http
,net
,crypto
등의 시스템 레벨 API를 제공
2. 동작 모델
2.1. 싱글 스레드 기반 이벤트 루프
- Node.js는 단일 스레드(single thread)에서 실행되며, 이벤트 루프를 통해 다수의 비동기 작업을 처리한다.
- Java나 C++의 멀티스레드 모델과 다르며, 병렬 처리가 아닌 비동기 처리 방식으로 고성능을 달성한다.
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
- 이 예시에서 파일을 읽는 동안 CPU는 블로킹되지 않고 다른 작업을 계속 수행할 수 있다.
2.2. 논블로킹 I/O
- I/O 작업(네트워크 요청, 디스크 접근 등)은 백그라운드 스레드로 넘기고, 결과가 준비되면 콜백이나 프라미스를 통해 처리한다.
- 이는 대규모 연결 처리에 적합한 구조이다 (예: 웹 서버, 실시간 채팅 등)
3. 특징
항목 | 설명 |
---|---|
런타임 환경 | V8 기반 JavaScript 실행 환경 |
I/O 모델 | 논블로킹, 이벤트 기반 |
스레드 구조 | 메인 로직은 싱글 스레드, I/O는 백그라운드 쓰레드풀 이용 |
주요 용도 | 웹 서버, API 서버, CLI 도구, 마이크로서비스 |
핵심 라이브러리 | fs , http , crypto , events , child_process 등 |
패키지 관리자 | npm (Node Package Manager) |
모듈 시스템 | CommonJS (require ) → 이후 ESM(import ) 지원도 추가됨 |
4. 장단점
장점 | 단점 |
---|---|
빠른 실행 속도 (V8) | CPU 바운드 작업에 부적합 |
비동기 처리에 최적화 | 콜백 지옥, 복잡한 에러 핸들링 |
npm 생태계 풍부 | 모듈 취약점 가능성 존재 |
자바스크립트로 풀스택 가능 | 싱글 스레드 특성 이해 필요 |
V8
V8은 구글이 만든 고성능 JavaScript 엔진으로, JavaScript 코드를 해석하고 실행하는 핵심 엔진이다.
크롬 브라우저와 Node.js의 핵심 구성요소이기도 하다.
컴파일
V8은 JavaScript를 실행하기 위해 다음과 같은 단계별 처리과정을 가진다:
1. 파싱(Parsing)
- JavaScript 소스코드를 구문 트리 AST(Abstract Syntax Tree) 로 변환로 변환한다.
2. 인터프리터 (Ignition)
- 처음에는 Ignition이라는 인터프리터가 코드를 바이트코드(Bytecode)로 변환하여 빠르게 실행한다.
-
이는 초기 실행 속도를 빠르게 하기 위한 것이다.
이 바이트코드는 V8이 해석(execute)하는 일종의 중간 코드 자바의 .class 처럼 외부 파일로 저장되진 않음 (메모리 상에서만 존재)
3. JIT 컴파일러 (TurboFan)
- 자주 실행되는 코드(hot code)에 대해서는 TurboFan이라는 JIT(Just-In-Time) 컴파일러가 바이트코드를 네이티브 머신코드로 컴파일한다.
-
이 단계에서 성능 최적화가 이루어진다.
머신 코드(= 2진수 코드) 로 컴파일해서 CPU에서 바로 실행되도록 만듦
즉, V8은 “해석기(인터프리터)”와 “컴파일러”를 모두 포함하는 엔진이며, 컴파일러만을 의미하는 것은 아니다.
구분 | 설명 |
---|---|
V8 | JavaScript 엔진 전체, 파서 + 인터프리터 + JIT 컴파일러 포함 |
Ignition | 인터프리터, 바이트코드로 변환하여 빠르게 실행 |
TurboFan | JIT 컴파일러, 바이트코드를 최적화된 네이티브 코드로 변환 |
역할 | JavaScript를 해석하고, 실행하고, 최적화까지 수행함 |
참고
V8의 발전은 과거 Crankshaft
, FullCodegen
등의 컴포넌트에서 지금은 Ignition + TurboFan
조합으로 발전해 왔으며, ES6 이상의 최신 JavaScript 기능도 빠르게 반영하고 있다.
ES6 vs CommonJS
ES6(ECMAScript 2015)와 CommonJS는 자바스크립트에서 모듈 시스템을 다루는 두 가지 대표적인 방식이다. 둘은 목적은 같지만, 작동 방식과 철학이 다르다.
1. CommonJS
개요
- Node.js의 기본 모듈 시스템
- JavaScript를 서버 사이드에서 사용하기 위해 만들어진 표준
- 동기적(synchronous) 로딩 방식
- 모듈을 즉시 실행하고 객체로 반환함
문법
// export
function add(a, b) {
return a + b;
}
module.exports = add;
// import
const add = require('./add');
console.log(add(2, 3)); // 5
특징
require()
는 런타임 시점에 모듈을 불러온다.- 모든 파일은 기본적으로 모듈이며,
exports
객체를 통해 외부 노출이 가능하다. - 동기 로딩이기 때문에 브라우저 환경에는 부적절 (브라우저는 I/O 비용이 크기 때문)
2. ES6 Modules (ESM)
개요
- ECMAScript 2015 (ES6)에서 도입된 공식 모듈 시스템
- 브라우저 및 Node.js 모두에서 사용 가능
- 정적(static) 로딩 → 빌드 타임에 모듈 종속성을 파악 가능
import
,export
키워드 사용
문법
// export
export function add(a, b) {
return a + b;
}
// import
import { add } from './add.js';
console.log(add(2, 3)); // 5
특징
- 정적 구조: 코드 파싱 시점에 모듈 구조를 파악할 수 있어 최적화에 유리
- 트리 셰이킹(tree shaking) 가능: 사용하지 않는 export는 번들링 시 제거
import
는 비동기적으로 동작하므로, 브라우저 환경에서도 적합
3. 주요 차이점
항목 | CommonJS | ES6 Modules |
---|---|---|
정의된 시기 | 2009년경 | 2015년 (ES6) |
실행 시점 | 런타임 | 파싱 타임 |
로딩 방식 | 동기적 | 비동기적 |
문법 | require , module.exports |
import , export |
사용 환경 | Node.js 전용 | 브라우저 + Node.js |
디폴트 export | module.exports = obj |
export default obj |
4. Node.js에서의 ES6 지원
- Node.js는
v13+
부터 ESM을 정식 지원함 - 하지만,
.js
확장자를 그대로 사용할 경우type: "module"
을package.json
에 명시해야 한다
{
"type": "module"
}
또는 파일 확장자를 .mjs
로 사용
5. 혼용 주의점
- ES6 모듈에서 CommonJS 모듈을 불러오는 것은 가능하지만, CommonJS에서는 ESM을 직접 import할 수 없음 (특히 동기 방식 문제로 인해)
- 두 시스템은 동작 방식이 달라 호환성 이슈가 있을 수 있음
→ 특히__dirname
,__filename
,require
등은 ESM에서 기본 제공되지 않음
결론
요약 |
---|
CommonJS는 Node.js 초기에 탄생한 서버 중심의 동기 모듈 시스템이다. |
ES6 Modules는 표준화된 정적 모듈 시스템으로, 트리 셰이킹과 브라우저 호환에 유리하다. |
현대 프로젝트에서는 ESM을 기본으로 하고, CommonJS는 레거시 지원이나 특정 패키지 사용 시에 함께 쓰인다. |
ESM을 더 많이 쓰는 이유
ESM을 더 많이 쓰는 이유
-
표준이다 - ESM은 [ECMAScript 2015 (ES6)]에서 도입된 공식 자바스크립트 모듈 표준임 - CommonJS(
require
)는 Node.js에서 만든 독자적인 방식이었고, 브라우저에서는 기본적으로 동작하지 않음 - 요즘은 Node도 브라우저도 ESM을 기본적으로 지원함 - 정적 분석 가능
-
import
는 정적(import 시점이 명확)이기 때문에- 트리 쉐이킹(Tree Shaking)이 가능함 → 사용하지 않는 모듈은 빌드 시 제거
- 타입 분석, 자동 완성, 리팩토링 등이 더 쉬워짐
- 브라우저에서 바로 사용 가능
<script type="module">
으로 ESM을 브라우저에서 직접 쓸 수 있음- CommonJS는 브라우저에서 안 됨 (번들링 필요)
- Top-level await
- ESM은 모듈 최상단에서도
await
를 쓸 수 있음 →top-level await
- CommonJS는 함수 내부에서만
await
가능
- ESM은 모듈 최상단에서도
비동기로 모듈을 불러오는가?
-
📦 ESM의
import
는 기본적으로 비동기임js import('./module.js') // 동적 import, 비동기 (Promise 반환)
-
🔍 정적 import (일반적인
import
문)js import x from './x.js' // 이건 사실 "정적이고 동기적으로 보이지만" 내부적으로는 비동기 초기화임
- 브라우저에서는 네트워크를 통해 모듈을 불러오니까 실제로는 **비동기적으로 로딩됨**
- Node.js도 `.mjs`나 `"type": "module"` 사용 시 내부적으로 비동기 초기화 단계가 있음
> 즉, **ESM은 기본적으로 비동기 초기화**를 염두에 두고 설계된 시스템이야
> 그래서 `import()` 문법도 **Promise 기반의 비동기**로 동작함
- 🔁 CommonJS vs ESM 비교
| 항목 | CommonJS (`require`) | ESM (`import`) |
|------|----------------------|----------------|
| 로딩 방식 | 동기 | 비동기 |
| 실행 시점 | 실행 중 즉시 | 모듈 로딩 단계에서 |
| 브라우저 지원 | ❌ (X) | ✅ (O) |
| Top-level await | ❌ | ✅ |
| 트리 쉐이킹 | ❌ | ✅ |
| 동적 로딩 | `require()` | `import()` (Promise 반환) |
결론
ESM은 공식 표준이고, 브라우저/Node 어디서나 사용 가능하며
정적 분석, 트리 쉐이킹, top-level await 등 다양한 최신 기능을 지원하기 때문에 더 많이 쓰임
스코프(scope)
JavaScript에서 “스코프(scope)”란 변수나 함수에 접근할 수 있는 범위를 의미한다.
“어디에서 어떤 변수에 접근할 수 있는가”를 결정하는 규칙이다.
이는 코드 실행 중 변수의 유효 범위를 이해하는 데 핵심적인 개념이다.
1. 스코프의 종류
1) 전역 스코프 (Global Scope)
- 함수 밖에서 선언된 변수는 어디서든 접근 가능
- 전역 객체(window, global 등)의 프로퍼티로 연결됨
var a = 10;
function print() {
console.log(a); // 10
}
print();
2) 함수 스코프 (Function Scope)
- 함수 내부에서 선언된 변수는 해당 함수 내부에서만 접근 가능
- 즉, function 키워드로 정의된 함수 내부에서만 접근
var
,let
,const
모두 해당
function test() {
var x = 5;
console.log(x); // 5
}
console.log(x); // ReferenceError
3) 블록 스코프 (Block Scope) (ES6부터)
{ }
로 감싸진 블록 내에서만 유효let
,const
는 블록 스코프를 가지며var
는 그렇지 않다
if (true) {
let y = 20;
const z = 30;
}
console.log(y); // ReferenceError
if (true) {
var w = 50;
}
console.log(w); // 50 ← var는 블록 스코프가 아니다
2. 렉시컬 스코프 (Lexical Scope)
자바스크립트는 렉시컬 스코프(Lexical Scope), 즉 정적 스코프를 따른다.
이는 “변수를 어디서 선언했는지”에 따라 스코프가 결정된다는 의미이다.
function outer() {
const a = 1;
function inner() {
console.log(a); // 1 ← outer의 스코프에 접근
}
inner();
}
outer();
inner()
는 정의된 위치에 따라outer()
의 변수a
에 접근할 수 있다.- 호출 위치가 아니라 선언 위치 기준으로 스코프를 본다.
3. 중첩 스코프 (Nested Scope)
스코프는 중첩될 수 있으며, 안쪽 함수는 바깥 스코프에 접근 가능하다.
const x = 1;
function outer() {
const x = 10;
function inner() {
console.log(x); // 10 ← 가장 가까운 스코프부터 찾음
}
inner();
}
outer();
→ 스코프 체인(scope chain)을 따라 가장 가까운 선언을 먼저 찾는다.
4. 주의할 점: var
, let
, const
의 스코프 차이
구분 | var |
let / const |
---|---|---|
스코프 | 함수 스코프 | 블록 스코프 |
중복 선언 | 가능 | 불가능 |
호이스팅 | O (초기화 undefined) | O (TDZ 적용) |
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError
let b = 2;
var
는 호이스팅(끌어올리기) 되어 초기화 없이 접근 가능하지만undefined
이다.let
,const
는 Temporal Dead Zone(TDZ) 구간에 걸려 ReferenceError가 발생한다.
요약
개념 | 설명 |
---|---|
스코프 | 변수/함수가 유효한 코드의 범위 |
전역 스코프 | 어디서든 접근 가능 |
함수 스코프 | 함수 내에서만 유효 |
블록 스코프 | { } 내에서만 유효 (let , const 만 해당) |
렉시컬 스코프 | 선언 위치 기준으로 스코프 결정 |
스코프 체인 | 안쪽 함수는 바깥 스코프 변수에 접근 가능 |
Promise
Promise
는 자바스크립트의 비동기 처리를 위한 객체이다.
미래에 어떤 작업이 성공하거나 실패할 것이라는 약속을 표현한다.
즉, 아직 값이 없지만 언젠가는 사용할 수 있는 값에 대한 핸들러이다.
1. 기본 개념
Promise
는 세 가지 상태를 가진다:
pending
(대기 중): 아직 결과를 알 수 없음fulfilled
(이행됨): 작업이 성공적으로 완료됨rejected
(거부됨): 작업이 실패함
한 번 fulfilled
또는 rejected
로 바뀌면 상태는 불변이다.
2. 기본 사용법
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("성공");
// reject("실패");
}, 1000);
});
promise.then(result => {
console.log(result); // "성공"
}).catch(error => {
console.error(error);
});
resolve(value)
: 비동기 작업이 성공했을 때 호출reject(reason)
: 실패했을 때 호출.then()
: 성공 시 실행할 콜백.catch()
: 실패 시 실행할 콜백.finally()
: 성공/실패와 무관하게 무조건 실행되는 콜백
3. 체이닝
.then()
은 새 Promise를 반환하므로 체이닝이 가능하다.
doSomething()
.then(result => doSomethingElse(result))
.then(finalResult => console.log(finalResult))
.catch(err => console.error(err));
이런 체이닝을 통해 비동기 작업을 순차적으로 처리할 수 있다.
4. 병렬 실행 - Promise.all
여러 Promise를 병렬로 실행하고, 모두 성공하면 결과 배열을 반환한다.
Promise.all([fetch1(), fetch2()])
.then(([result1, result2]) => {
console.log(result1, result2);
})
.catch(err => {
console.error("하나라도 실패함", err);
});
동시에 시작되고, 모든 작업이 끝날 때까지 기다리는 구조 (일일히 해도 똑같지만 묶음이 더 효율적)
5. 순서 무관 병렬 - Promise.race
, Promise.any
Promise.race
: 가장 먼저 끝난 Promise의 결과를 반환Promise.any
: 하나라도 fulfilled되면 성공으로 간주, 모두 실패하면 에러 반환
6. 예외 처리
.catch()
또는 try/catch
(async/await
과 함께 사용)로 에러를 처리한다.
new Promise((resolve, reject) => {
throw new Error("에러 발생");
}).catch(err => {
console.error(err.message); // 에러 발생
});
7. 정리
개념 | 설명 |
---|---|
Promise |
비동기 작업의 미래 값을 나타내는 객체 |
resolve() |
작업 성공 |
reject() |
작업 실패 |
.then() |
성공 핸들링 |
.catch() |
실패 핸들링 |
.finally() |
항상 실행되는 마무리 |
결론
Promise
는 비동기 로직을 더 구조화되고 예측 가능하게 만들며,
콜백 지옥을 해결하는 기반이 된다.
또한 async/await
은 Promise
위에서 동작하므로, Promise의 동작 원리를 이해하는 것이 필수적이다.
클로저(Closure)
클로저는 함수가 생성될 당시의 외부 변수 스코프를 기억하고, 함수가 그 스코프 밖에서 호출되더라도 접근할 수 있는 기능을 말한다.
다시 말해, 함수가 자신이 선언될 때의 렉시컬 스코프(lexical scope)를 기억하는 것이다.
📌 예시 1: 기본적인 클로저
function outer() {
let x = 10;
function inner() {
console.log(x);
}
return inner;
}
const closureFunc = outer();
closureFunc(); // 10
inner()
는outer()
의 지역변수x
에 접근하고 있다.outer()
는 이미 실행이 끝났지만,inner()
는x
를 기억하고 있다.- 이것이 클로저다.
📌 예시 2: 반복문에서 발생하는 클로저 문제
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
- 위의 코드에서 모든
funcs[i]()
가3
을 출력하는 이유는var
로 선언된i
가 함수 스코프를 가지기 때문. i
는 하나의 공유된 변수이므로, 루프가 끝났을 때i === 3
이 되어 모두 같은 값을 참조하게 된다.
클로저 문제의 원인
- 루프나 비동기 환경에서
var
를 사용할 때, 모든 함수가 같은 변수를 참조함. - 이로 인해 원하지 않는 결과가 발생할 수 있다.
해결 방법
let
을 사용해서 블록 스코프 변수로 선언
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2
- 즉시 실행 함수(IIFE)를 사용하여 클로저 생성
const funcs = [];
for (var i = 0; i < 3; i++) {
(function(j) {
funcs.push(function() {
console.log(j);
});
})(i);
}
클로저의 장점
- 상태 유지 (private 변수처럼 사용)
- 캡슐화 (외부에서 직접 접근 불가능)
- 함수형 프로그래밍에 적합한 구조
클로저의 단점 또는 주의점
- 메모리 누수: 불필요한 참조를 유지하면 가비지 컬렉션 되지 않음
- 의도하지 않은 변수 공유로 인해 버그 발생 가능
결론
- 클로저는 함수가 외부 변수의 스코프를 기억하는 현상이다.
- 주로 콜백, 반복문, 비동기 로직에서 유용하지만, 잘못 사용하면 예상치 못한 동작을 유발할 수 있다.
- 클로저는 자바스크립트의 강력한 기능이지만, 스코프와 변수 생명 주기에 대한 이해가 필수적이다.
콜백 함수(callback function)
다른 함수에 인자로 넘겨져서, 특정 시점에 실행되는 함수를 의미한다.
개념 설명
- 자바스크립트는 함수를 값처럼 취급할 수 있는 일급 객체 언어다.
- 따라서 함수를 다른 함수에 인자로 전달하거나, 함수 내부에서 실행할 수 있다.
- 이렇게 전달된 함수가 호출되는 구조를 콜백(callback)이라고 한다.
예제
function greet(name, callback) {
console.log("안녕하세요, " + name);
callback(); // 전달받은 함수 실행
}
function afterGreeting() {
console.log("환영합니다.");
}
greet("홍길동", afterGreeting);
출력:
안녕하세요, 홍길동
환영합니다.
afterGreeting
은greet
에 인자로 전달된 콜백 함수다.greet
는 이름을 출력한 뒤, 콜백으로 전달된 함수를 실행한다.
콜백은 언제 유용한가?
- 비동기 작업의 완료 시점에 실행할 코드를 전달할 때 유용하다.
- 예: 파일을 다 읽은 뒤, 서버 응답을 받은 뒤, 버튼을 누른 뒤 등.
예: 비동기 콜백
setTimeout(() => {
console.log("3초 후 실행됨");
}, 3000);
- 위 코드에서
()=>{}
는setTimeout
의 콜백이다. - 3초 뒤 이벤트 루프에 의해 실행된다.
정리
- 콜백 함수는 “나중에 실행하기 위해 전달하는 함수”다.
- 주로 비동기 작업이 완료되었을 때 실행된다.
- Promise, async/await 같은 비동기 패턴도 내부적으로는 콜백 기반으로 구성된다.
📌 콜백은 “지금 실행하는 게 아니라, 필요할 때 호출해줘“라고 함수에 부탁하는 것과 같다.
Event Loop
-
이벤트 루프는 Node.js가 단일 JavaScript 스레드를 사용함에도 불구하고 비동기 I/O를 처리할 수 있게 해주는 메커니즘이다.
-
이는 가능한 경우 작업을 시스템 커널에 위임함으로써 가능하다.
-
대부분의 최신 커널은 멀티스레드이기 때문에, 백그라운드에서 여러 작업을 동시에 처리할 수 있다.
-
커널은 작업이 완료되면 Node.js에 알려주고, 해당 콜백은 poll 큐에 추가되어 나중에 실행된다.
Node.js 수행 구조
[사용자 JS 코드]
↓
[V8 엔진 (JS 실행)]
↓
[Node.js 바인딩]
↓
[libuv (이벤트 루프 + 비동기 I/O)]
↓
[OS 커널 기능 (epoll, kqueue 등)]
이벤트루프와 V8엔진의 코드 실행은 모두 하나의 스레드에서 일어난다. 어떻게 이것이 가능할까?
바로, “동시에”가 아니라 “순차적으로 번갈아가면서” 돌아간다. 큐 ↔ 스택이 교대로 동작한다.
-
예시1
console.log('1'); setTimeout(() => { console.log('2'); }, 0); console.log('3');
- Call Stack에 console.log(‘1’) 실행 → 출력: 1
- setTimeout은 비동기 → 콜백 함수는 Web API 영역으로 넘기고 대기
- 다음 console.log(‘3’) 실행 → 출력: 3
- 현재 JS 코드 실행(Call Stack)이 다 끝남
- 이제 Event Loop가 등장!
- “콜백 큐에 들어온 거 있어?” → setTimeout 콜백 찾음
- Call Stack이 비었으니, console.log(‘2’) 콜백 Push
- console.log(‘2’) 실행 → 출력: 2
-
예시2
const fs = require('fs'); console.log('A'); fs.readFile('file.txt', 'utf8', (err, data) => { console.log('B'); }); console.log('C');
- console.log(‘A’) → Call Stack 실행 → 출력: A
- fs.readFile(…) 실행
- 내부적으로 libuv가 I/O 요청을 OS에 맡김 (이건 JS 밖, C++ 영역에서 돌아감)
- 콜백은 저장해두고, Stack은 바로 빠짐 → 비동기니까 기다리지 않음
- console.log(‘C’) → Stack 실행 → 출력: C
- Call Stack이 비어짐 → 이벤트 루프 작동 시작
- OS가 파일 읽기 끝내고 → libuv → 콜백을 Task Queue에 넣음
- 이벤트 루프가 콜백 꺼냄 → Call Stack에 넣고 실행 → 출력: B
이벤트 루프가 콜백 큐에서 작업을 가져오는 타이밍은 Call Stack이 완전히 비었을 때이다. “Call Stack이 비었을 때” + “Task Queue에 뭔가 있으면” 이벤트 루프가 동작하는 것이다.
📌 즉, JavaScript 코드가 실행되다가 함수 종료(=Stack Pop) 를 통해 완전히 비워지면, 그때 이벤트 루프가 큐(Task Queue or Microtask Queue)에서 작업을 꺼내서 실행한다.
📌 Microtask vs Task setTimeout, setInterval → Task Queue Promise.then, queueMicrotask, MutationObserver → Microtask Queue 그리고 Microtask가 항상 Task보다 먼저 실행된다.
콜스택이 비어있다는 전제하에,
- 마이크로 태스크 큐에 있는 마이크로 태스크를 FIFO로 순차 실행한다.
- 마이크로 태스크 큐가 비면, 렌더링 작업을 수행한다.
- 렌더링 작업 후에는 매크로 태스크 큐(=태스크 큐)에 있는 태스크를 실행한다.
- 매크로 태스크 큐의 작업이 1개 실행되고, 다시 1번으로 돌아간다 매크로 태스크 1개 => 마이크로 태스크 전부 => 렌더링작업 수행 => 매크로 태스크 1개 => …의 반복이다.
“I/O 작업의 콜백 함수는, 현재 Call Stack이 완전히 비워진 후에 이벤트 루프가 꺼내서 실행한다.”
동작 방식
- 단계(phase)
┌───────────────────────────┐ ┌─>│ timers │ // setTimeout, setInterval │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ // 일부 I/O 콜백 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ // 내부용 │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ // setImmediate │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ // 소켓 등 close 이벤트 └───────────────────────────┘
-
각 단계는 FIFO 큐 방식의 콜백 큐를 가지고 있다.
-
이벤트 루프가 특정 단계에 진입하면:
-
해당 단계에 특화된 작업을 수행하고,
-
큐에 있는 콜백들을 최대 수 또는 큐가 소진될 때까지 실행한다.
-
이후 다음 단계로 넘어간다.
-
이벤트 루프가 한 번 돌 때마다, Node.js는 남아 있는 비동기 I/O나 타이머가 있는지 확인한다.
-
아무 것도 없으면 정상적으로 종료(clean shutdown) 된다.
timers (타이머 단계)
이 단계는 timers라고 표현했지만 Task Queue가 1개 수행되는 단계이다. 대상은 setTimeout, setInterval이다. 여러개가 있어도 1개가 수행된다.
타이머는 일정 시간이 지난 이후에 콜백이 실행될 수 있도록 지정하는 메커니즘이다. 타이머의 콜백은 지정된 시간이 지난 뒤 가능한 한 빨리 실행되도록 예약되나, 운영체제의 스케줄링이나 다른 콜백의 실행으로 인해 지연될 수 있다.
pending callbacks (보류된 콜백 단계)
이 단계에서는 TCP 오류 같은 특정 시스템 작업에 대한 콜백이 실행된다. 예를 들어, TCP 소켓이 연결 중 ECONNREFUSED 오류를 받으면, 일부 유닉스 계열 시스템은 오류를 지연 보고하도록 설계되어 있으며, 이 콜백은 pending callbacks 단계에 실행된다.
poll (폴링 단계)
Micro Task Queue가 남김없이 전부 수행된다. 이벤트가 끝나 콜백을 받은 것들이 큐에 Fifo로 쌓여있다가 수행되는 단계이다.
poll 단계는 두 가지 주요 기능을 수행한다:
-
블로킹할 시간(대기 시간)을 계산한다.
-
poll 큐의 이벤트들을 처리한다.
이벤트 루프가 poll 단계에 진입할 때 타이머가 없으면, 다음 두 가지 중 하나가 발생한다:
-
poll 큐가 비어있지 않으면, 큐의 콜백을 반복적으로 실행한다. 큐가 비거나, 시스템이 설정한 최대 한도에 도달할 때까지 진행한다.
-
poll 큐가 비어있으면, 다시 두 가지 중 하나가 발생한다:
-
setImmediate()로 예약된 스크립트가 있으면, poll 단계를 종료하고 check 단계로 넘어간다.
-
예약된 스크립트가 없으면, 큐에 콜백이 추가될 때까지 대기한 후 즉시 실행한다.
-
poll 큐가 비게 되면, 이벤트 루프는 타이머의 임계시간 도달 여부를 확인하고, 도달한 타이머가 있다면 timers 단계로 되돌아가 콜백을 실행한다.
check (체크 단계)
check 단계는 setImmediate()로 등록한 콜백들이 실행되는 전용 단계이다.
setTimeout() → timers 단계에서 실행됨 I/O 콜백 → poll 단계에서 실행됨 setImmediate() → check 단계에서 실행됨
setImmediate() vs setTimeout()
setImmediate()와 setTimeout()은 유사하지만, 호출 시점에 따라 동작 방식이 달라진다.
setImmediate()는 poll 단계가 완료된 후 실행된다.
setTimeout()은 지정한 최소 시간(ms) 이후에 실행되도록 예약된다.
process.nextTick() vs setImmediate()
process.nextTick()은 같은 이벤트 루프 사이클 내에서 즉시 실행된다.
setImmediate()는 다음 이벤트 루프 사이클에서 실행된다.
이름이 직관적이지 않지만, process.nextTick()이 실제로 더 즉시 실행된다. 이름을 바꾸는 것은 레거시 모듈 호환성 문제 때문에 어렵다.
Node.js 팀은 가급적이면 setImmediate() 사용을 권장한다. 이쪽이 이벤트 루프 흐름을 예측하기 쉽기 때문이다.
nextTick 사용처
- 오류 처리, 정리 작업 또는 재시도를 이벤트 루프가 계속되기 전에 수행하고자 할 때
- 콜 스택이 정리된 이후, 이벤트 루프가 계속되기 전에 콜백을 실행하고자 할 때
Quiz
1. var, let, const 에 대해 설명해주실 수 있을까요?
var
, let
, const
는 자바스크립트에서 변수를 선언할 때 사용하는 키워드이다.
var
는 함수 스코프를 가지며, 중복 선언이 가능하고 선언 전에 접근해도 undefined
가 된다. 이는 호이스팅 때문이다.
let
은 블록 스코프를 가지며, 중복 선언이 불가능하고 선언 전에 접근 시 에러가 발생한다.
const
는 let
과 동일한 블록 스코프를 가지지만, 선언과 동시에 초기화를 해야 하며, 재할당이 불가능하다. 단, 객체 내부 값은 변경 가능하다.
2. Promise란 무엇인지 설명해주실 수 있을까요?
Promise
는 비동기 작업의 성공 또는 실패를 표현하는 자바스크립트 객체이다.
pending
, fulfilled
, rejected
의 세 가지 상태를 가진다.
성공 시 .then()
, 실패 시 .catch()
, 완료 후 .finally()
로 후속 작업을 정의할 수 있다.
콜백 지옥을 해결하기 위해 등장한 구조로, async/await
문법과 함께 사용되기도 한다.
3. Hoisting이란 무엇인지 설명해주실 수 있을까요?
호이스팅은 변수나 함수 선언이 코드 상단으로 끌어올려지는 자바스크립트의 동작 방식이다.
var
로 선언된 변수는 선언과 정의가 끌어올려지며, 초기값은 undefined
가 된다.
반면 let
, const
는 선언만 호이스팅되며, 초기화 이전에는 접근할 수 없어 ReferenceError
가 발생한다.
함수 선언문은 전체가 끌어올려지므로 선언 전에 호출할 수 있다.
4. async/await란 무엇인지 설명해주실 수 있을까요?
async/await
는 Promise
기반 비동기 코드를 동기처럼 작성할 수 있게 해주는 문법이다.
async
키워드는 함수가 항상 Promise
를 반환하도록 만들며, 내부에서 await
를 사용할 수 있다.
await
는 Promise가 처리될 때까지 실행을 일시 정지시키고, 결과값을 반환한다.
에러 처리를 위해 try/catch
와 함께 사용된다.
5. Arrow Function이란 무엇인지 설명해주실 수 있을까요?
화살표 함수는 function
키워드를 대신하여 더 간결하게 함수를 정의할 수 있는 문법이다.
this
, arguments
, super
를 바인딩하지 않고 상위 스코프의 값을 그대로 사용한다.
이 때문에 this
가 중요한 컨텍스트에서는 주의가 필요하다.
간단한 콜백이나 일회성 함수에 자주 사용된다.
6. ‘==’와 ‘===’ 연산자의 차이는 무엇인지 설명해주실 수 있을까요?
==
는 느슨한 동등 비교로, 타입이 다를 경우 암묵적 형 변환을 거쳐 비교한다.
예를 들어 '1' == 1
은 true
이다.
===
는 엄격한 동등 비교로, 타입과 값이 모두 같아야 true
를 반환한다.
형 변환이 없기 때문에 버그를 줄이기 위해 ===
사용이 권장된다.
7. Express란 무엇이고 왜 필요하며 대안은 무엇이 있는지 설명해주실 수 있을까요?
Express는 Node.js에서 가장 널리 사용되는 웹 애플리케이션 프레임워크이다.
간결한 라우팅, 미들웨어 기반 아키텍처, 다양한 플러그인을 통해 서버 개발을 빠르게 할 수 있게 해준다.
필요성은 Node.js의 기본 HTTP 모듈이 너무 저수준이기 때문에 추상화가 필요하기 때문이다.
대안으로는 Koa, Fastify, NestJS 등이 있다.
8. npm이란 무엇인지 설명해주실 수 있을까요?
npm은 Node.js의 기본 패키지 매니저로, 외부 라이브러리 설치, 의존성 관리, 스크립트 실행 등을 담당한다.
package.json
을 통해 프로젝트 설정과 의존성 정보를 관리하며, npm install
, npm run
등의 명령어로 사용된다.
9. 깊은 복사와 얕은 복사의 차이는 무엇이고 JS에서 각각을 구현하는 방법은 어떻게 되는지
얕은 복사는 객체의 참조값만 복사하여 원본과 복사본이 같은 메모리를 참조한다.
깊은 복사는 객체 내부의 값까지 재귀적으로 모두 복사하여 완전히 독립적인 객체를 만든다.
얕은 복사는 Object.assign
, 전개 연산자({...obj}
)로 할 수 있다.
깊은 복사는 structuredClone(obj)
또는 JSON.parse(JSON.stringify(obj))
방법이 있다.
단, JSON 방식은 undefined
, Date
, Function
등은 올바르게 처리하지 못한다.
10. JS의 passed by value 와 passed by reference 에 대해 아는 만큼 설명해주실 수 있을까요?
자바스크립트는 원시 타입은 값에 의한 전달(pass by value), 객체 타입은 참조에 의한 전달(pass by reference)로 처리한다.
값에 의한 전달은 복사본이 전달되어 원본에 영향을 주지 않는다.
참조에 의한 전달은 같은 메모리 주소를 공유하므로, 함수 내에서 객체를 변경하면 원본도 바뀐다.
11. 고차 함수란 무엇인지
고차 함수는 함수를 인자로 받거나 함수를 반환하는 함수이다.
자바스크립트에서는 map
, filter
, reduce
, forEach
등이 대표적인 고차 함수이다.
함수를 일급 객체로 취급하는 언어에서 자주 사용되며, 추상화와 재사용성에 유리하다.
12. Node.js는 single-threaded 기반 JS 런타임입니다. 이에 대해 아는 만큼 설명해주실 수 있을까요?
Node.js는 단일 스레드 기반으로 동작하는 런타임이지만, I/O 처리를 위해 이벤트 루프와 백그라운드 쓰레드(libuv 스레드풀)를 사용한다.
JS 코드는 싱글 스레드로 실행되지만, 파일 읽기, DB 통신 등은 비동기적으로 백그라운드에서 처리되고, 완료되면 메인 루프로 다시 콜백이 전달된다.
이 구조는 높은 동시성을 지원하지만 CPU 집중 작업에는 부적합하다.
13. Node.js는 non-blocking, asynchronous 기반 JS 런타임입니다. 이에 대해 아는 만큼 설명해주실 수 있을까요?
Node.js는 모든 I/O 작업을 비동기, 논블로킹 방식으로 처리한다.
작업 요청 후 완료까지 기다리지 않고, 콜백 또는 Promise를 통해 결과를 처리한다.
이 덕분에 블로킹 없이 수천 개의 요청을 동시에 처리할 수 있는 고성능 서버를 구축할 수 있다.
14. Node.js의 이벤트 루프란 무엇이고 왜 필요하며 어떻게 작동하는지 아는 만큼 설명해주실 수 있을까요?
이벤트 루프는 Node.js에서 비동기 처리를 담당하는 핵심 구조이다.
콜 스택, 태스크 큐, 마이크로태스크 큐를 이용해 작업을 스케줄링한다.
이벤트 루프는 무한 루프를 돌며 큐에 쌓인 콜백들을 하나씩 꺼내 실행한다.
비동기 작업의 결과를 처리하는 시점과 순서를 결정하기 때문에 필수적인 구조이다.
15. 트랜스파일러와 번들러에 대해 설명해주실 수 있을까요?
트랜스파일러는 소스 코드를 다른 버전이나 언어로 변환하는 도구이다.
예: TypeScript → JavaScript, ES6 → ES5. Babel이 대표적이다.
번들러는 여러 파일을 하나의 파일로 묶는 도구로, Webpack, Rollup, Vite 등이 있다.
최적화, 의존성 분석, 코드 분할 등을 수행하여 웹 애플리케이션 성능을 향상시킨다.
16. 클로저(Closure)란?
클로저는 함수가 선언될 당시의 스코프를 기억하여, 함수 외부에서 내부 변수를 계속 참조할 수 있는 구조이다.
예를 들어, 함수 내부에서 선언된 변수가 외부 함수 종료 후에도 유지된다면 그게 클로저이다.
데이터 은닉, 상태 유지, 캡슐화 등에 활용된다.
17. async await와 Promise 차이
async/await
는 Promise
를 더 간편하게 사용하기 위한 문법이다.
Promise
는 .then().catch()
로 체이닝하지만, async/await
는 동기처럼 작성 가능하고 try/catch
로 예외 처리가 간결하다.
내부적으로는 동일하게 Promise
를 사용한다.
18. Blocking과 Non-blocking의 차이
Blocking은 어떤 작업이 완료될 때까지 다음 작업을 멈추고 기다리는 방식이다. 반면 Non-blocking은 작업을 요청한 후 바로 다음 작업을 실행하며, 결과는 나중에 콜백이나 Promise 등의 방식으로 처리한다. Node.js는 기본적으로 Non-blocking I/O를 기반으로 동작하여 높은 처리량을 가능하게 한다.
2. Circular Dependency는 무엇이며, Node.js에서 이를 어떻게 처리하나요?
Circular Dependency는 두 개 이상의 모듈이 서로를 참조하면서 순환 참조가 발생하는 상황을 말한다. Node.js에서는 이를 감지하고 순환 구조가 발생할 경우 해당 모듈의 exports 객체를 비워진 상태로 반환하거나, 아직 완전히 초기화되지 않은 객체를 반환한다. 이로 인해 예기치 않은 동작이 발생할 수 있으므로, 순환 참조는 설계 단계에서 피하는 것이 좋다. 구조 분리나 의존성 주입을 통해 해결할 수 있다.
3. require.cache가 무엇인지 알고 있나요? 이를 활용한 상황을 설명해주세요.
require.cache는 Node.js가 require()
로 불러온 모듈을 캐싱하는 객체이다. 같은 모듈을 여러 번 불러도 캐시에 저장된 객체를 반환함으로써 성능을 높인다. 이를 활용하면 테스트 환경에서 특정 모듈을 강제로 다시 로드하거나, 핫 리로딩 기능을 구현할 때 유용하게 쓸 수 있다. 예를 들어 delete require.cache[require.resolve('./module')]
를 사용해 모듈을 초기화할 수 있다.
4. Promise, async/await 사용 시의 에러 처리는 어떻게 하면 좋을까요?
Promise는 .catch()
로 에러를 처리하고, async/await는 try...catch
문으로 에러를 처리한다. 또한 최상위 async 함수에서는 .catch()
를 붙이거나 전역 unhandledRejection
이벤트 리스너를 등록해 처리하지 못한 예외를 핸들링할 수 있다. 항상 비동기 함수 내부에 try...catch
를 명확하게 작성하는 것이 안정성을 높인다.
5. Node.js 애플리케이션에서 메모리 누수를 어떻게 감지하고 디버깅할 수 있나요?
메모리 누수는 지속적으로 증가하는 메모리 사용량으로 나타나며, process.memoryUsage()
나 heapdump
, clinic.js
, chrome devtools
의 프로파일링 기능 등을 통해 감지할 수 있다. 또한 --inspect
옵션을 이용해 브라우저에서 힙 스냅샷을 비교 분석하거나, 이벤트 리스너가 해제되지 않는 경우 EventEmitter.listenerCount()
등을 활용해 추적할 수 있다.
6. Express에서 미들웨어의 흐름을 설명해주세요. 비동기 미들웨어에서 에러 처리 방법은요?
Express에서 미들웨어는 요청(req), 응답(res), 그리고 next()
함수를 인자로 가지며, next()
를 호출함으로써 다음 미들웨어로 흐름이 이동한다. 미들웨어는 순차적으로 실행되며, 중간에 응답을 보내면 이후 미들웨어는 실행되지 않는다. 비동기 미들웨어에서 에러가 발생하면 next(err)
를 호출하거나, try-catch 문에서 next(error)
로 넘겨주어야 에러 핸들러 미들웨어로 흐름이 전달된다.
7. PM2란
PM2는 Node.js 애플리케이션을 프로덕션 환경에서 관리할 수 있게 도와주는 프로세스 매니저이다. 클러스터 모드를 통해 멀티코어 활용이 가능하며, 애플리케이션 자동 재시작, 로드 밸런싱, 로깅, 상태 모니터링, 설정 파일 기반 배포 등을 지원한다.
8. CI/CD 환경에서 Node.js 프로젝트를 배포할 때 주의해야 할 점
Node.js 프로젝트를 CI/CD 환경에서 배포할 때 다음과 같은 점을 주의해야 한다.
node_modules
는 환경마다 다를 수 있으므로 배포 시npm ci
를 통해 의존성을 정확하게 설치해야 한다..env
와 같은 환경 변수는 보안 문제로 별도 관리해야 하며, CI/CD 파이프라인에 안전하게 주입되어야 한다.- 빌드와 테스트는 배포 전에 반드시 자동화되어야 하며, 실패 시 배포가 중단되어야 한다.
- 애플리케이션 로그, 에러 추적, 헬스 체크 등을 통해 모니터링을 설정해야 운영 중 문제를 빠르게 감지할 수 있다.