Node.js 백엔드 면접 준비

Node.js 면접 질문과 답변 2026

백엔드 개발자를 위한 집중형 Node.js 면접 세트입니다. 답변은 말로 연습하기 좋게 구성되어 있으며, 아키텍처를 설명하고, 예시를 들고, 운영상 트레이드오프를 언급하도록 되어 있습니다.

Node.js 모의 면접 시작신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

아키텍처와 이벤트 루프

1Node.js란 무엇이며 주로 어디에 사용되나요?

Node.js는 브라우저 외부에서 실행되는 JavaScript 런타임으로, V8 엔진 기반으로 구축되었으며 네트워크, 파일, 프로세스, 스트림 및 운영체제 접근을 위한 API를 제공합니다. 주로 REST API, GraphQL 서비스, 실시간 WebSocket 서버, 마이크로서비스, CLI(Command Line Interface) 도구, 프록시, API 게이트웨이 등에 널리 쓰입니다. Node.js의 핵심 강점은 비차단 I/O(non-blocking I/O)로, 단일 프로세스로도 수많은 네트워크 연결을 효율적으로 처리할 수 있다는 점입니다. 반면 트레이드오프로 워커 스레드나 별도 프로세스, 외부 서비스로 분리하지 않으면 CPU 연산이 오래 걸리는 작업이 이벤트 루프를 차단할 수 있습니다.

AI 코치와 함께 이 질문에 답해 보세요

2Node.js는 내부적으로 어떻게 동작하나요?

JavaScript는 V8에서 실행되는 반면, Node.js API는 비동기 작업을 운영체제나 libuv의 스레드 풀에 위임합니다. 메인 JavaScript 스레드는 I/O 작업이 완료되기를 기다리지 않고 다른 코드를 계속해서 실행합니다. 작업이 완료되면 해당 콜백이나 Promise 후속 작업이 이벤트 루프에서 처리될 준비를 마칩니다. 이러한 방식을 통해 Node.js는 사용자 JavaScript가 대개 단일 메인 스레드에서 실행됨에도 불구하고 높은 동시성을 달성합니다.

AI 코치와 함께 이 질문에 답해 보세요

3V8, libuv, 이벤트 루프(event loop)란 각각 무엇인가요?

V8은 JavaScript를 컴파일 및 실행하고, JavaScript 객체와 메모리를 관리합니다. libuv는 크로스 플랫폼 이벤트 루프, 비동기 I/O 연동, 그리고 파일 시스템 작업 일부나 DNS, 암호화(crypto) 연산 등을 처리하는 워커 스레드 풀을 제공합니다. 이벤트 루프는 완료된 작업의 콜백이 언제 실행되어야 하는지 결정합니다. Node.js 자체는 JavaScript API를 네이티브 기능과 연결해 주는 런타임이며, V8 단독으로는 HTTP 서버, 파일 시스템 접근, 프로세스 API 등을 제공하지 않습니다.

AI 코치와 함께 이 질문에 답해 보세요

4Node.js를 흔히 싱글 스레드라고 부르는 이유는 무엇인가요?

단일 Node.js 프로세스는 일반적으로 하나의 메인 스레드에서 사용자 JavaScript를 실행하므로, 두 개의 일반적인 요청 핸들러가 해당 스레드에서 완전히 동시에 JavaScript를 실행하지는 않습니다. 그러나 플랫폼 자체가 문자 그대로 하나의 스레드로만 동작하는 것은 아닙니다. libuv는 스레드 풀을 보유하고 있고, 운영체제는 비동기 I/O를 수행하며, V8은 가비지 컬렉션을 수행하고, Node.js는 `worker_threads` 및 자식 프로세스도 지원합니다. 따라서 이 표현은 주로 기본 JavaScript 실행 모델을 설명하는 것입니다.

AI 코치와 함께 이 질문에 답해 보세요

5Node.js는 어떻게 많은 요청을 동시에 처리할 수 있나요?

Node.js는 요청마다 스레드를 차단(blocking)하는 대신 입출력(I/O) 작업을 시작한 후 곧바로 이벤트 루프로 제어를 반환합니다. 예를 들어 1,000건의 데이터베이스 호출이 진행 중인 동안에도 메인 스레드는 계속해서 새로운 연결을 수락하고 준비된 콜백을 처리할 수 있습니다. 이러한 방식은 HTTP 호출, 데이터베이스 쿼리, 파일 시스템 및 소켓 작업과 같은 I/O 바운드 워크로드에 매우 적합합니다. 반면 요청 핸들러에서 무거운 동기 CPU 연산을 수행하면 이벤트 루프가 차단되어 다른 모든 요청 처리가 지연되므로 성능이 크게 저하됩니다.

AI 코치와 함께 이 질문에 답해 보세요

6Node.js 이벤트 루프의 주요 단계는 무엇인가요?

Node.js는 타이머 처리와 함께 libuv의 pending callbacks, idle/prepare, poll, check, close callbacks 등의 단계를 사용합니다. poll은 많은 I/O(입출력) 콜백을 처리하고, check는 setImmediate를 실행하며, 타이머는 최소 지연 시간이 지나고 스케줄링이 허용하면 setTimeout/setInterval 콜백을 실행합니다. libuv 1.45와 Node.js 20 이상에서는 매 반복의 poll 뒤에 타이머를 처리하고, 호환성을 위해 루프 진입 전에도 초기 타이머 처리를 수행합니다. process.nextTick은 별도의 Node 큐를, Promise 반응과 queueMicrotask는 V8 마이크로태스크 큐를 사용합니다. 둘 다 libuv 단계가 아닙니다. 이 큐들은 콜백 사이의 체크포인트에서 처리되며, 재귀적으로 계속 채우면 다른 작업이 실행 기회를 잃을 수 있습니다.

AI 코치와 함께 이 질문에 답해 보세요

7동기, 비동기, 논블로킹 작업은 어떻게 다른가요?

동기는 제어 흐름을 설명합니다. 호출자가 계속 실행되기 전에 호출이 결과를 반환하거나 예외를 던집니다. 비동기는 나중에 콜백, Promise 또는 다른 알림으로 결과를 받는 방식입니다. 블로킹은 작업이나 자원을 기다리는 동안 현재 스레드가 다른 일을 못 하는 상태입니다. 논블로킹 인터페이스는 ‘실행하면 블록됨’이라는 결과를 반환하거나 알림을 준비하는 식으로 그 대기를 피합니다. 이들은 서로 다른 차원입니다. 동기라고 반드시 I/O(입출력) 준비를 기다리는 것은 아니며, Promise를 반환한다고 무거운 동기 계산이 논블로킹이 되지도 않습니다. Node.js의 fs.readFileSync는 블로킹 동기 인터페이스입니다. fs.promises.readFile은 비동기로 결과를 제공하며, 그동안 메인 스레드는 다른 일을 처리할 수 있습니다. 요청 처리에서는 적합한 비동기 I/O 인터페이스를 우선합니다.

import fs from 'node:fs';

const syncData = fs.readFileSync('file.txt', 'utf8');

const asyncData = await fs.promises.readFile(
  'file.txt',
  'utf8'
);
AI 코치와 함께 이 질문에 답해 보세요

8이벤트 루프 기아 상태란 무엇인가요?

동기 JavaScript나 계속 채워지는 체크포인트 큐 때문에 I/O(입출력) 콜백, 타이머, 요청을 제때 처리하지 못하는 상태입니다. 무거운 계산, 대규모 JSON 처리, 재귀적으로 예약되는 마이크로태스크, 과도한 process.nextTick 사용 등이 원인입니다. 입력 크기를 제한하고, 비싼 작업을 프로파일링하며, 계산량이 많은 작업을 worker_threads나 다른 서비스로 옮깁니다. 작업을 나눌 때는 setImmediate 등으로 이후 이벤트 루프 순환에 실제로 제어권을 넘겨야 합니다. 이미 해결된 Promise를 반복해서 await하거나 queueMicrotask를 예약하는 것만으로는 여전히 I/O 기아가 발생할 수 있습니다. 자주 실행되는 요청 경로에서는 블로킹 동기 인터페이스를 피합니다.

app.get('/report', (req, res) => {
  const result = performVeryHeavyCalculation();
  res.json(result);
});
AI 코치와 함께 이 질문에 답해 보세요

비동기 프로그래밍과 오류

9콜백과 콜백 지옥이란 무엇인가요?

콜백은 다른 작업이 호출하도록 그 작업에 전달하는 함수입니다. Array.map처럼 동기 실행될 수도 있고 fs.readFile처럼 비동기 실행될 수도 있습니다. 전통적인 Node.js 비동기 콜백은 callback(error, result)처럼 첫 인수에 오류를 두는 관례를 흔히 사용합니다. 콜백 지옥은 의존적인 콜백이 깊게 중첩되어 제어 흐름과 오류 처리가 불분명해지는 상태입니다. 이름 있는 함수, 작은 단위의 작업, Promise, async/await로 가독성을 높일 수 있습니다. 독립적인 작업은 적절한 경우 Promise.all 등으로 함께 시작하되, 동시 실행 제한을 지키고 오류를 처리해야 합니다.

import fs from 'node:fs';

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(data);
});
AI 코치와 함께 이 질문에 답해 보세요

10Promise란 무엇이며 async/await는 어떻게 동작하나요?

Promise는 비동기 작업의 미래 결과를 나타내며 대기(pending), 이행(fulfilled), 거부(rejected) 상태를 가질 수 있습니다. async 함수는 항상 Promise를 반환합니다. await는 Promise가 처리(settle)될 때까지 현재 async 함수만을 일시 중단하며, Node.js 프로세스 전체를 차단하지는 않습니다. 좋은 답변이라면 try/catch를 통한 에러 처리, 처리되지 않고 방치되는 Promise(floating Promise) 방지, 그리고 독립적인 작업들이 불필요하게 순차적으로 실행되지 않도록 Promise.all을 활용하는 방법 등을 언급해야 합니다.

async function loadUser(id) {
  try {
    const user = await repository.findById(id);
    return user;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

const [user, settings] = await Promise.all([
  getUser(),
  getSettings()
]);
AI 코치와 함께 이 질문에 답해 보세요

11process.nextTick, queueMicrotask, setImmediate, setTimeout은 어떻게 다른가요?

process.nextTick은 Node의 next-tick 큐에 작업을 예약합니다. queueMicrotask는 Promise 반응도 사용하는 V8 마이크로태스크 큐를 사용합니다. 일반적인 CommonJS 최상위 체크포인트에서는 nextTick 콜백이 대기 중인 Promise/queueMicrotask보다 먼저 실행됩니다. ES 모듈 최상위 평가는 이미 마이크로태스크 문맥에서 이루어지므로 대기 중인 마이크로태스크가 nextTick보다 먼저 실행될 수 있습니다. 이는 문맥에 따라 달라지며, 실행 중인 JavaScript를 중단하는 보편적 우선순위 규칙이 아닙니다. setImmediate는 check에서 실행됩니다. setTimeout(fn, 0)도 최소 타이머 시간을 기다리므로 즉시 실행되지 않습니다. 둘의 순서는 예약 위치에 달려 있지만 같은 I/O(입출력) 콜백 안에서 예약하면 setImmediate가 먼저 실행됩니다. nextTick 고유 동작이 필요하지 않다면 이식 가능한 마이크로태스크 지연에는 queueMicrotask를 우선합니다.

// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
AI 코치와 함께 이 질문에 답해 보세요

12Node.js에서는 오류를 어떻게 처리해야 하나요?

동기 예외에는 try/catch를 사용합니다. Promise 기반 작업은 try/catch 안에서 await하거나 거부 처리기를 연결합니다. 바깥의 동기 try/catch는 나중에 실행되는 콜백의 예외를 잡지 못합니다. 오류가 첫 인수인 콜백은 해당 인수를 검사해야 하며 EventEmitter의 error 이벤트에는 적절한 처리기가 필요합니다. 서버는 요청 오류 처리를 중앙화하고 유용한 문맥을 기록하며 내부 정보를 클라이언트에 노출하지 않아야 합니다. uncaughtException 이후 안전하게 계속 실행할 수 있다고 가정하면 안 됩니다. 필요한 동기 비상 정리만 수행하고 프로세스를 종료한 뒤 감독 프로세스가 재시작하도록 합니다. 계획된 정상 종료에는 점진적 종료가 유용하지만, 잡히지 않은 예외 후 임의의 비동기 복구는 신뢰할 수 있는 전략이 아닙니다.

try {
  JSON.parse(input);
} catch (error) {
  handleError(error);
}

try {
  await operation();
} catch (error) {
  handleError(error);
}

operation((error, result) => {
  if (error) return handleError(error);
  return result;
});
AI 코치와 함께 이 질문에 답해 보세요

모듈과 프로젝트 관리

13Node.js에서 모듈이란 무엇인가요?

모듈은 명시적으로 내보낸(export) 인터페이스를 갖춘 격리된 코드 단위입니다. Node.js는 node:fs 및 node:http와 같은 내장 모듈, 로컬 프로젝트 모듈, npm 패키지, CommonJS 모듈, ECMAScript 모듈을 지원합니다. 모듈을 사용하면 책임 분리가 명확해지고 재사용성이 높아집니다. 프로덕션 환경에서의 트레이드오프는 결합도(coupling)입니다. 모듈 간에 서로 너무 깊게 의존하거나 모듈을 가져올(import) 때 부수 효과(side effect)를 숨겨두면 테스트와 애플리케이션 시작 시점의 동작을 파악하기가 어려워집니다.

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
AI 코치와 함께 이 질문에 답해 보세요

14CommonJS require와 ES 모듈 import는 어떻게 다른가요?

CommonJS는 require와 module.exports를, ES 모듈은 import와 export를 사용합니다. require는 보통 동기적으로 로드하며 조건부 호출이 가능합니다. 정적 import 선언은 의존성 그래프를 분석 가능하게 하고, 동적 import()는 Promise를 반환합니다. 최신 Node.js는 지원되는 상호운용 규칙에 따라 최상위 await가 없는 ES 모듈 그래프도 require로 로드할 수 있으므로 ES 모듈이 항상 비동기 평가를 뜻하지는 않습니다. .cjs/.mjs 확장자, package.json의 type, 적용되는 구문 감지 규칙이 모듈 방식을 결정합니다. ES 모듈은 최상위 await를 지원합니다. CommonJS는 __dirname/__filename을 제공하고, 현재 Node.js는 파일 기반 ES 모듈에 import.meta.dirname/filename을 제공합니다. import.meta.url은 URL 기반 경로 해석과 이전 버전 호환 방식에 유용합니다.

// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };

// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
AI 코치와 함께 이 질문에 답해 보세요

15npm, package.json, package-lock.json은 무엇인가요?

npm은 패키지 관리용 명령줄 인터페이스입니다. package.json은 메타데이터, 스크립트, 직접 의존성의 요구 사항을 기술하며 흔히 버전 범위를 사용합니다. package-lock.json은 결정된 의존성 트리와 패키지 버전 및 무결성 정보를 기록합니다. 애플리케이션의 잠금 파일은 버전 관리에 포함합니다. npm ci는 지속적 통합에 유용합니다. 잠금 파일로 설치하고 기존 node_modules 디렉터리를 제거하며 package.json과 불일치하면 실패합니다. 잠금 파일은 의존성의 변동을 줄이지만 운영체제, 네이티브 빌드, 생명주기 스크립트, Node 버전, 설치 옵션까지 같게 만들지는 않습니다. 재현 가능한 빌드에는 통제된 도구 체인과 환경도 필요합니다.

{
  "name": "api",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "node --test"
  },
  "dependencies": {},
  "devDependencies": {}
}
AI 코치와 함께 이 질문에 답해 보세요

16설정과 환경 변수는 어떻게 관리해야 하나요?

환경 변수는 process.env로 읽습니다. 존재하는 값은 문자열이며 없는 속성은 undefined입니다. 시작할 때 필수 값을 검증하고 의도적으로 변환합니다. PORT를 Number로 바꾸는 것만으로는 빈 문자열, NaN, 범위를 벗어난 포트를 거부하지 못합니다. 필수 설정이 잘못되면 명확한 오류로 중단합니다. 실제 비밀 값은 버전 관리에서 제외하고, 운영 환경의 비밀 값은 배포 플랫폼이나 비밀 관리 도구로 전달합니다. 비밀이 없는 예제 설정은 저장소에 포함할 수 있습니다. NODE_ENV는 애플리케이션 관례이지 신뢰할 수 있는 보안 경계가 아닙니다.

const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
  throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
AI 코치와 함께 이 질문에 답해 보세요

이벤트, 버퍼 및 스트림

17EventEmitter란 무엇인가요?

EventEmitter는 Node.js 프로세스 내에서 발행-구독(publish-subscribe) 패턴을 구현합니다. on은 리스너를 등록하고, once는 1회성 이벤트 리스너를 등록하며, off는 리스너를 제거하고, emit은 등록된 순서대로 리스너를 동기적으로 호출합니다. 이러한 동기적 특성은 매우 중요한데, 리스너 처리가 느리면 이벤트를 발생시킨 호출자도 차단되기 때문입니다. error 이벤트는 특별히 취급되어 처리되지 않은 error 이벤트가 발생하면 프로세스가 비정상 종료될 수 있습니다. 또한 오래 실행되는 시스템에서는 메모리 누수를 방지하기 위해 사용하지 않는 리스너를 명시적으로 제거해야 합니다.

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

emitter.on('orderCreated', order => {
  console.log(`Order ${order.id} created`);
});

emitter.emit('orderCreated', { id: 42 });
AI 코치와 함께 이 질문에 답해 보세요

18Buffer는 어떤 용도로 사용되나요?

`Buffer`는 고정된 크기의 바이트 시퀀스를 나타냅니다. 파일 처리, TCP 소켓, HTTP 본문, 이미지, 암호화 및 바이너리 프로토콜 등에 사용됩니다. `Buffer.from`은 기존 데이터로부터 버퍼를 생성하고, `Buffer.alloc`은 0으로 채워진 메모리를 할당하며, `Buffer.allocUnsafe`는 더 빠르지만 덮어쓰기 전까지 이전 메모리 내용이 남아 있을 수 있습니다. 따라서 `allocUnsafe`는 메모리를 즉시 채우고 오래된 데이터를 외부에 절대 노출하지 않는 경우에만 적합합니다.

const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
AI 코치와 함께 이 질문에 답해 보세요

19스트림(stream)이란 무엇이며 어떤 종류가 있나요?

스트림은 모든 데이터를 한 번에 메모리에 로드하는 대신 청크(chunk) 단위로 나누어 처리합니다. 주요 유형으로는 `Readable`, `Writable`, `Duplex`, `Transform`이 있습니다. 파일, HTTP 요청 및 응답 객체, TCP 소켓, gzip 스트림 등이 대표적인 예시입니다. 스트림은 메모리 사용량과 지연 시간을 줄여주기 때문에 대용량 파일이나 네트워크 트래픽을 다룰 때 매우 유용합니다. 프로덕션 수준의 답변이라면 스트림 체인 전체에서 오류가 올바르게 처리되어야 한다는 점도 함께 언급해야 합니다.

import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
try {
  await pipeline(
    fs.createReadStream('large.log'),
    fs.createWriteStream('copy.log')
  );
} catch (error) {
  console.error('Copy failed:', error);
  process.exitCode = 1;
}
AI 코치와 함께 이 질문에 답해 보세요

20배압(backpressure)이란 무엇이며, pipeline은 왜 유용한가요?

배압(backpressure)은 데이터 소스가 청크를 생성하는 속도가 목적지에서 소비하는 속도보다 빠를 때 발생합니다. 배압 처리가 없으면 메모리 사용량이 계속 증가하여 프로세스가 느려지거나 비정상 종료될 수 있습니다. 스트림과 `pipe`로 데이터 흐름을 조율할 수 있지만, `node:stream/promises`의 `pipeline`은 에러를 전파하고 연관된 스트림을 닫으며 `Promise`를 반환하므로 다단계 체인에서 훨씬 안전합니다. 이는 파일 압축, 업로드, 다운로드, 스트림 변환 작업의 기본 방식으로 사용하기에 적합합니다.

import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';

await pipeline(
  fs.createReadStream('input.txt'),
  zlib.createGzip(),
  fs.createWriteStream('input.txt.gz')
);
AI 코치와 함께 이 질문에 답해 보세요