면접 준비 5

공보경·2025년 6월 23일

면접 준비

목록 보기
5/5

웹기술 면접준비 가이드 5 - JavaScript & 웹 보안

JavaScript 이벤트 루프와 CORS 보안 정책에 대한 포괄적인 면접 준비 가이드

목차

  1. JavaScript 이벤트 루프 (Event Loop)
  2. CORS (Cross-Origin Resource Sharing)
  3. JavaScript 이벤트 루프 심화 학습

1. JavaScript 이벤트 루프 (Event Loop)

📖 정의

JavaScript 이벤트 루프는 JavaScript 엔진이 비동기 작업을 처리하는 메커니즘으로, Call Stack, Web API, Callback Queue, Microtask Queue를 순환하며 단일 스레드에서 비동기 처리를 가능하게 하는 핵심 구조입니다.

🏗️ 핵심 구성 요소

구성요소설명역할
Call Stack함수 호출 스택현재 실행 중인 함수들의 스택 구조
Web API브라우저 제공 APIDOM, setTimeout, HTTP 요청 등 비동기 처리
Callback Queue콜백 대기열매크로태스크(Macrotask) 대기 공간
Microtask Queue마이크로태스크 큐Promise, queueMicrotask 등 고우선순위 작업
Event Loop이벤트 순환기Queue의 작업을 Call Stack으로 이동

💡 이벤트 루프 동작 원리

┌─────────────────┐    ┌──────────────┐    ┌─────────────────┐
│   Call Stack    │    │   Web API    │    │ Callback Queue  │
│                 │    │              │    │                 │
│ [executing...]  │    │ setTimeout() │    │ [callback1]     │
│ [function2]     │    │ HTTP Request │    │ [callback2]     │
│ [function1]     │    │ DOM Events   │    │                 │
└─────────────────┘    └──────────────┘    └─────────────────┘
         ↑                       ↓                    ↑
         │                       │                    │
         └───────────Event Loop──────────────────────┘
                    (Stack이 비어있을 때만 이동)

┌─────────────────┐
│ Microtask Queue │
│                 │
│ [Promise.then]  │ ← 높은 우선순위 (Callback Queue보다 먼저 처리)
│ [queueMicro]    │
└─────────────────┘

🔄 실행 순서 및 우선순위

  1. Call Stack 실행: 동기 코드 우선 실행
  2. Microtask Queue 처리: Promise.then, queueMicrotask
  3. Callback Queue 처리: setTimeout, setInterval, DOM 이벤트
  4. 렌더링: 브라우저 화면 업데이트 (필요시)

📝 실제 동작 예시

console.log('1'); // 동기 - 즉시 실행

setTimeout(() => {
  console.log('2'); // 매크로태스크 - 4번째 실행
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 마이크로태스크 - 3번째 실행
});

console.log('4'); // 동기 - 2번째 실행

// 출력 순서: 1 → 4 → 3 → 2

🎯 매크로태스크 vs 마이크로태스크

구분매크로태스크마이크로태스크
종류setTimeout, setInterval, I/OPromise.then, queueMicrotask, async/await
우선순위낮음높음
실행 시점한 번에 하나씩 처리큐가 빌 때까지 모두 처리
예시사용자 이벤트, 네트워크 응답Promise 체이닝, DOM 변경

⚡ 성능 최적화 포인트

1. 긴 작업 분할

// ❌ 나쁜 예: 블로킹 발생
function heavyTask() {
  for (let i = 0; i < 1000000; i++) {
    // 오래 걸리는 작업
  }
}

// ✅ 좋은 예: 작업 분할
function heavyTaskOptimized() {
  let i = 0;
  function process() {
    let start = Date.now();
    while (i < 1000000 && Date.now() - start < 16) {
      // 16ms 이내로 작업 제한
      i++;
    }
    if (i < 1000000) {
      setTimeout(process, 0); // 다음 이벤트 루프에서 계속
    }
  }
  process();
}

2. 마이크로태스크 남용 방지

// ❌ 나쁜 예: 무한 마이크로태스크
function badMicrotask() {
  Promise.resolve().then(badMicrotask); // 무한 루프
}

// ✅ 좋은 예: 적절한 사용
function goodMicrotask() {
  Promise.resolve().then(() => {
    // 필요한 작업만 수행
    console.log('작업 완료');
  });
}

✅ 장점

  • 비동기 처리: 단일 스레드에서 비동기 작업 처리 가능
  • 논블로킹: UI 응답성 유지
  • 효율적 자원 사용: 스레드 생성 오버헤드 없음
  • 예측 가능한 실행: 명확한 실행 순서 규칙

❌ 주의사항

  • 콜백 헬: 중첩된 콜백으로 인한 가독성 저하
  • 블로킹 위험: 동기 코드가 길어질 경우 전체 애플리케이션 블로킹
  • 복잡한 디버깅: 비동기 실행으로 인한 디버깅 어려움
  • 메모리 누수: 콜백 참조로 인한 메모리 누수 가능성

🎯 면접 답변 템플릿 (30초)

"JavaScript 이벤트 루프는 단일 스레드 환경에서 비동기 처리를 가능하게 하는 메커니즘입니다. Call Stack에서 동기 코드를 실행하고, Web API에서 비동기 작업을 처리한 후, Microtask Queue와 Callback Queue를 통해 작업을 순서대로 처리합니다. Microtask(Promise)가 Macrotask(setTimeout)보다 높은 우선순위를 가지며, 이를 통해 논블로킹 비동기 프로그래밍이 가능합니다."

📚 참고 링크


2. CORS (Cross-Origin Resource Sharing)

📖 정의

CORS는 웹 브라우저에서 다른 출처(Origin)의 리소스에 접근할 수 있도록 허용하는 보안 정책입니다. Same-Origin Policy의 제약을 안전하게 완화하여, 신뢰할 수 있는 교차 출처 요청을 가능하게 하는 HTTP 헤더 기반 메커니즘입니다.

🏗️ 출처(Origin) 구성 요소

Origin = Protocol + Host + Port

구성요소설명예시
Protocol통신 프로토콜http://, https://
Host도메인 또는 IPexample.com, 192.168.1.1
Port포트 번호:80, :443, :3000

💡 Same-Origin vs Cross-Origin

URLOrigin동일 출처?이유
https://example.com/page1기준 URL모든 요소 동일
https://example.com:443/page2https://example.comHTTPS 기본 포트 443
http://example.com/page3http://example.com프로토콜 다름 (http ≠ https)
https://sub.example.com/page4https://sub.example.com호스트 다름
https://example.com:8080/page5https://example.com:8080포트 다름

🔄 CORS 동작 과정

1. Simple Request (단순 요청)

Client                          Server
  │                               │
  │ GET /api/data                 │
  │ Origin: https://client.com    │
  │ ──────────────────────────► │
  │                               │
  │ 200 OK                        │
  │ Access-Control-Allow-Origin:  │
  │ https://client.com            │
  │ ◄────────────────────────── │

2. Preflight Request (예비 요청)

Client                          Server
  │                               │
  │ OPTIONS /api/data             │ ← 예비 요청
  │ Origin: https://client.com    │
  │ Access-Control-Request-Method │
  │ ──────────────────────────► │
  │                               │
  │ 200 OK                        │ ← 예비 응답
  │ Access-Control-Allow-*        │
  │ ◄────────────────────────── │
  │                               │
  │ POST /api/data                │ ← 실제 요청
  │ Origin: https://client.com    │
  │ ──────────────────────────► │
  │                               │
  │ 201 Created                   │ ← 실제 응답
  │ Access-Control-Allow-Origin   │
  │ ◄────────────────────────── │

🎯 CORS 요청 유형

1. Simple Request 조건

  • HTTP Methods: GET, HEAD, POST
  • Content-Type:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • Headers: 기본 안전 헤더만 사용

2. Preflight Request 발생 조건

  • HTTP Methods: PUT, DELETE, PATCH
  • Content-Type: application/json, text/xml
  • Custom Headers: Authorization, X-Custom-Header 등

3. Credentialed Request (인증 요청)

// 클라이언트 설정
fetch('https://api.example.com/data', {
  method: 'POST',
  credentials: 'include', // 쿠키, 토큰 포함
  headers: {
    'Content-Type': 'application/json'
  }
});

🛡️ CORS 헤더 설정

서버 응답 헤더

헤더설명예시
Access-Control-Allow-Origin허용할 출처* 또는 https://example.com
Access-Control-Allow-Methods허용할 HTTP 메서드GET, POST, PUT, DELETE
Access-Control-Allow-Headers허용할 요청 헤더Content-Type, Authorization
Access-Control-Allow-Credentials인증 정보 허용 여부true
Access-Control-Max-AgePreflight 캐시 시간86400 (24시간)
Access-Control-Expose-Headers클라이언트 접근 허용 헤더X-Total-Count

클라이언트 요청 헤더

헤더설명자동 설정
Origin요청 출처✅ 브라우저 자동
Access-Control-Request-Method실제 요청 메서드✅ Preflight 시
Access-Control-Request-Headers실제 요청 헤더✅ Preflight 시

📝 실제 서버 구현 예시

Node.js + Express

const express = require('express');
const app = express();

// CORS 미들웨어
app.use((req, res, next) => {
  // 특정 출처 허용
  res.header('Access-Control-Allow-Origin', 'https://trusted-site.com');
  
  // 또는 동적 출처 허용
  const allowedOrigins = ['https://app1.com', 'https://app2.com'];
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Max-Age', '86400');
  
  // Preflight 요청 처리
  if (req.method === 'OPTIONS') {
    res.sendStatus(200);
  } else {
    next();
  }
});

Spring Boot

@Configuration
public class CorsConfig {
  
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://trusted-site.com"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
  }
}

🔧 CORS 해결 방법

1. 서버 측 해결 (권장)

// Express.js CORS 설정
const cors = require('cors');
app.use(cors({
  origin: ['https://example.com', 'https://app.example.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

❓ 왜 서버 측 해결이 권장되는가?

🔐 보안 측면
이유설명위험성
보안 정책의 본질서버가 "누구에게 리소스 접근을 허용할지" 결정클라이언트 우회 시 보안 정책 훼손
브라우저 보안 모델Origin 헤더 조작을 브라우저가 자동 차단악성 사이트의 도메인 위장 방지
// ❌ 클라이언트에서 Origin 헤더 조작 시도 (불가능)
fetch('https://api.example.com/data', {
  headers: {
    'Origin': 'https://trusted-site.com' // 브라우저가 무시함
  }
});
🎯 기술적 측면
방법서버 측 해결클라이언트 측 해결
지속성✅ 영구적 해결❌ 임시적 해결
적용 범위✅ 모든 클라이언트❌ 특정 환경만
유지보수✅ 중앙 집중적❌ 분산된 관리
프로덕션 적용✅ 가능❌ 불가능
🏢 실무적 한계
// ❌ 프로덕션에서 불가능한 클라이언트 측 해결책들
- Chrome 확장 프로그램: 모든 사용자가 설치할 수 없음
- 브라우저 설정 변경: 일반 사용자에게 요구할 수 없음
- 개발 서버 프록시: 프로덕션 배포 시 의미없음

// ✅ 프로덕션에서 유일한 해결책
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
💡 실제 시나리오: 은행 API 예시
// ✅ 은행이 승인한 안전한 방식
// "우리 고객센터 웹사이트에서만 계좌 정보 접근 허용"
res.header('Access-Control-Allow-Origin', 'https://bank-customer.com');

// ❌ 클라이언트 측 우회 (위험한 방식)
// "브라우저 설정을 바꿔서 은행 API에 접근하자"
// → 은행의 의도와 무관하게 보안을 우회하는 것
📋 서버 측 해결이 권장되는 핵심 이유
  1. 보안 책임: 리소스 소유자(서버)가 접근 권한을 결정해야 함
  2. 표준 준수: W3C CORS 표준의 의도된 사용법
  3. 실용성: 프로덕션 환경에서 유일하게 동작하는 방법
  4. 일관성: 모든 클라이언트, 모든 브라우저에서 동일하게 적용
  5. 유지보수: 중앙에서 정책 관리 가능
  6. 보안 무결성: 클라이언트 측 우회는 보안 정책을 훼손

2. 프록시 서버 사용

// Vite 개발 서버 프록시 설정
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});

3. JSONP (레거시)

// JSONP 콜백 방식 (보안상 권장하지 않음)
function handleResponse(data) {
  console.log(data);
}

const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.head.appendChild(script);

⚠️ 보안 고려사항

1. 와일드카드 사용 주의

// ❌ 위험: 모든 출처 허용
res.header('Access-Control-Allow-Origin', '*');

// ✅ 안전: 특정 출처만 허용
const allowedOrigins = ['https://trusted1.com', 'https://trusted2.com'];
if (allowedOrigins.includes(req.headers.origin)) {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
}

2. Credentials와 와일드카드 동시 사용 금지

// ❌ 에러 발생
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');

// ✅ 정확한 설정
res.header('Access-Control-Allow-Origin', 'https://trusted-site.com');
res.header('Access-Control-Allow-Credentials', 'true');

3. 민감한 헤더 노출 방지

// ✅ 필요한 헤더만 노출
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Size');

🎯 CORS vs 기타 보안 정책

정책목적동작 위치설정 방법
CORS교차 출처 리소스 공유브라우저HTTP 헤더
CSP콘텐츠 보안 정책브라우저HTTP 헤더/메타 태그
SOP동일 출처 정책브라우저브라우저 기본 정책
CSRF Protection교차 사이트 요청 위조 방지서버토큰 검증

✅ 장점

  • 보안 강화: Same-Origin Policy의 안전한 완화
  • 유연성: 신뢰할 수 있는 교차 출처 통신 허용
  • 표준화: W3C 표준으로 브라우저 간 일관성
  • 세밀한 제어: 출처, 메서드, 헤더별 상세 제어 가능

❌ 단점

  • 복잡성: Preflight 요청으로 인한 추가 네트워크 오버헤드
  • 브라우저 의존: 서버 간 통신에는 적용되지 않음
  • 설정 복잡도: 올바른 설정을 위한 깊은 이해 필요
  • 디버깅 어려움: CORS 에러 메시지의 모호함

🎯 면접 답변 템플릿 (30초-1분)

"CORS는 웹 브라우저의 Same-Origin Policy를 안전하게 완화하여 다른 출처의 리소스에 접근할 수 있게 하는 보안 정책입니다. 서버에서 Access-Control-Allow-Origin 등의 헤더를 설정하여 신뢰할 수 있는 출처만 허용합니다. Simple Request는 즉시 처리되지만, POST with JSON 같은 복잡한 요청은 Preflight 검사를 통해 사전 확인 후 실제 요청이 전송됩니다. 개발 시에는 프록시 서버를 활용하고, 운영에서는 특정 도메인만 허용하여 보안을 유지하는 것이 중요합니다."

🚀 실무 팁

1. 개발 환경 설정

// package.json에서 프록시 설정
{
  "name": "my-app",
  "proxy": "http://localhost:8080"
}

// 또는 Webpack Dev Server 설정
devServer: {
  proxy: {
    '/api/*': {
      target: 'http://localhost:8080',
      secure: false,
      changeOrigin: true
    }
  }
}

2. 운영 환경 모니터링

// CORS 에러 로깅
window.addEventListener('unhandledrejection', (event) => {
  if (event.reason.name === 'TypeError' && 
      event.reason.message.includes('CORS')) {
    console.error('CORS Error:', event.reason);
    // 에러 리포팅 서비스로 전송
  }
});

📚 참고 링크


💡 추가 학습 권장사항

JavaScript 이벤트 루프 심화

  • Web Workers와 메인 스레드 통신
  • Service Workers의 이벤트 루프
  • Node.js 이벤트 루프와 브라우저의 차이점
  • 성능 프로파일링과 최적화 기법

CORS 보안 심화

  • Content Security Policy (CSP)와의 연관성
  • CORS Preflight 캐싱 전략
  • 다중 도메인 환경에서의 CORS 관리
  • 마이크로서비스 아키텍처에서의 CORS 설계

실무 응용

  • React/Vue.js에서의 CORS 처리
  • API Gateway에서의 CORS 설정
  • CDN과 CORS 설정
  • 모바일 앱에서의 CORS 고려사항

🔄 JavaScript 이벤트 루프 심화 학습

1. Web Workers와 메인 스레드 통신

🔍 Web Workers란?

Web Workers는 별도의 백그라운드 스레드에서 스크립트를 실행할 수 있게 해주는 API로, 메인 스레드를 차단하지 않고 무거운 작업을 수행할 수 있습니다.

참고 출처: Web Workers로 경험하는 자바스크립트의 멀티스레딩 - velog

🛠️ Web Workers 생성과 통신

Worker 생성
// main.js (메인 스레드)
const worker = new Worker('worker.js');

// 현대적인 번들러 환경에서는
const worker = new Worker(new URL('./worker.js', import.meta.url));

// worker.js (Worker 스레드)
self.addEventListener('message', function(e) {
  const data = e.data;
  // 무거운 작업 수행
  const result = heavyComputation(data);
  // 결과를 메인 스레드로 전송
  self.postMessage(result);
});
메세지 통신 (postMessage API)
// 메인 스레드 → Worker
worker.postMessage({
  command: 'start',
  data: [1, 2, 3, 4, 5]
});

// Worker → 메인 스레드
worker.addEventListener('message', function(e) {
  console.log('Worker 결과:', e.data);
});

// Worker에서 메인 스레드로 응답
self.postMessage({
  status: 'completed',
  result: computedData
});

🔐 WorkerGlobalScope와 제약사항

// Worker 내부에서 사용 가능한 전역 객체
self.console.log('Worker 내부');
self.setTimeout(() => {}, 1000);
self.fetch('https://api.example.com/data');

// ❌ Worker에서 사용 불가능
// document.getElementById() - DOM 접근 불가
// window.location - window 객체 접근 불가
// parent 객체 접근 불가

🚀 실제 사용 예시: 피보나치 계산

// main.js - 메인 스레드가 차단되지 않는 방식
const worker = new Worker('fibonacci-worker.js');

document.getElementById('calcBtn').addEventListener('click', () => {
  document.getElementById('result').innerText = '계산 중...';
  worker.postMessage(45); // Worker에게 숫자 전달
});

worker.onmessage = function(e) {
  document.getElementById('result').innerText = `결과: ${e.data}`;
};

// fibonacci-worker.js
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

onmessage = function(e) {
  const result = fibonacci(e.data);
  postMessage(result); // 결과를 메인 스레드로 전달
};
// Comlink를 사용한 RPC 스타일 통신
import { wrap } from 'comlink';

const worker = new Worker('math-worker.js');
const mathWorker = wrap(worker);

// 프로미스 기반으로 간편하게 사용
const result = await mathWorker.calculatePi(1000000);
console.log('π 계산 결과:', result);

// math-worker.js
import { expose } from 'comlink';

const api = {
  async calculatePi(iterations) {
    // 몬테카를로 방법으로 π 계산
    let insideCircle = 0;
    for (let i = 0; i < iterations; i++) {
      const x = Math.random() * 2 - 1;
      const y = Math.random() * 2 - 1;
      if (x * x + y * y <= 1) insideCircle++;
    }
    return (insideCircle / iterations) * 4;
  }
};

expose(api);

🔒 SharedArrayBuffer와 Atomics (보안 제약)

// ⚠️ SharedArrayBuffer는 Spectre/Meltdown 보안 이슈로 인해
// Cross-Origin-Embedder-Policy와 Cross-Origin-Opener-Policy 헤더가 필요

// 서버에서 다음 헤더 설정 필요:
// Cross-Origin-Embedder-Policy: require-corp
// Cross-Origin-Opener-Policy: same-origin

// SharedArrayBuffer를 사용한 메모리 공유 (보안 설정 시에만 가능)
if (typeof SharedArrayBuffer !== 'undefined') {
  const sharedBuffer = new SharedArrayBuffer(1024);
  const sharedArray = new Int32Array(sharedBuffer);

  // Worker로 공유 메모리 전달
  worker.postMessage({ sharedBuffer });

  // Worker에서 원자적 연산 수행
  // worker.js
  self.onmessage = function(e) {
    const sharedArray = new Int32Array(e.data.sharedBuffer);
    
    // 원자적 연산으로 경쟁 조건 방지
    Atomics.add(sharedArray, 0, 1);
    Atomics.store(sharedArray, 1, 42);
  };
} else {
  console.warn('SharedArrayBuffer는 지원되지 않습니다.');
}

2. Service Workers의 이벤트 루프

🔍 Service Worker 개념

Service Worker는 네트워크 프록시 역할을 하는 별도 스레드로, 웹 앱과 네트워크 사이에서 요청을 가로채고 캐싱을 관리합니다.

참고 출처:

🔄 Service Worker 생명주기와 이벤트 처리

// 메인 스레드에서 Service Worker 등록
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('SW 등록 성공:', registration.scope);
    })
    .catch(error => {
      console.log('SW 등록 실패:', error);
    });
}

// sw.js - Service Worker 라이프사이클 이벤트
self.addEventListener('install', event => {
  console.log('Service Worker 설치');
  // 정적 자원 캐싱
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/index.html', 
        '/app.js', 
        '/style.css',
        '/manifest.json'
      ]);
    })
  );
  // 대기 단계 건너뛰기
  self.skipWaiting();
});

self.addEventListener('activate', event => {
  console.log('Service Worker 활성화');
  // 이전 버전 캐시 정리
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== 'v1') {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  // 모든 클라이언트 제어하기
  return self.clients.claim();
});

self.addEventListener('fetch', event => {
  // 네트워크 요청 가로채기 (Cache First 전략)
  event.respondWith(
    caches.match(event.request).then(response => {
      // 캐시에 있으면 캐시에서 반환, 없으면 네트워크 요청
      return response || fetch(event.request).then(fetchResponse => {
        // 새로 받은 응답을 캐시에 저장
        if (fetchResponse.ok) {
          const responseClone = fetchResponse.clone();
          caches.open('v1').then(cache => {
            cache.put(event.request, responseClone);
          });
        }
        return fetchResponse;
      });
    }).catch(() => {
      // 오프라인 상태일 때 기본 페이지 반환
      return caches.match('/offline.html');
    })
  );
});

🎯 Service Worker vs 브라우저 이벤트 루프 차이점

Service Worker 이벤트 루프 특성:
  1. 독립적인 컨텍스트: 메인 스레드와 완전히 분리된 글로벌 스코프
  2. 이벤트 기반: install, activate, fetch, message, push, sync 이벤트로 동작
  3. 생명주기 관리: 브라우저가 필요에 따라 시작/종료
우선순위별 처리 체계 (브라우저 메인 스레드)
우선순위큐 종류처리 내용예시
1순위Microtask QueuePromise, queueMicrotaskPromise.resolve().then()
2순위Animation FramesrequestAnimationFrame애니메이션 렌더링
3순위Task Queue (Macrotask)setTimeout, DOM 이벤트setTimeout(), 클릭 이벤트

🌐 고급 Service Worker 패턴

백그라운드 동기화 (Background Sync)
// 메인 스레드에서 백그라운드 동기화 등록
navigator.serviceWorker.ready.then(registration => {
  registration.sync.register('send-message');
});

// Service Worker에서 동기화 처리
self.addEventListener('sync', event => {
  if (event.tag === 'send-message') {
    event.waitUntil(
      sendPendingMessages() // 대기 중인 메시지 전송
    );
  }
});

async function sendPendingMessages() {
  const messages = await getStoredMessages();
  return Promise.all(
    messages.map(message => 
      fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(message)
      }).then(() => removeStoredMessage(message.id))
    )
  );
}
푸시 알림 처리
// 푸시 이벤트 수신
self.addEventListener('push', event => {
  const options = {
    body: event.data ? event.data.text() : '새로운 알림',
    icon: '/icon-192.png',
    badge: '/badge-72.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: 1
    },
    actions: [
      {
        action: 'explore',
        title: '확인',
        icon: '/check.png'
      },
      {
        action: 'close',
        title: '닫기',
        icon: '/xmark.png'
      }
    ]
  };
  
  event.waitUntil(
    self.registration.showNotification('PWA 알림', options)
  );
});

// 알림 클릭 처리
self.addEventListener('notificationclick', event => {
  event.notification.close();
  
  if (event.action === 'explore') {
    // 특정 페이지로 이동
    event.waitUntil(
      clients.openWindow('/notifications')
    );
  }
});
메인 스레드와 Service Worker 통신
// 메인 스레드에서 Service Worker로 메시지 전송
if (navigator.serviceWorker.controller) {
  navigator.serviceWorker.controller.postMessage({
    type: 'UPDATE_CACHE',
    payload: { version: '2.0' }
  });
}

// Service Worker에서 메시지 수신
self.addEventListener('message', event => {
  if (event.data.type === 'UPDATE_CACHE') {
    event.waitUntil(updateCache(event.data.payload.version));
  }
  
  // 응답 전송
  event.ports[0].postMessage({
    status: 'success',
    message: 'Cache updated'
  });
});

// Client API를 사용한 모든 클라이언트에게 메시지 전송
self.clients.matchAll().then(clients => {
  clients.forEach(client => {
    client.postMessage({
      type: 'CACHE_UPDATED',
      version: '2.0'
    });
  });
});

3. Node.js 이벤트 루프와 브라우저의 차이점

🌐 브라우저 vs Node.js 환경 비교

참고 출처:

📊 기본 구조 차이점
구분브라우저Node.js
JavaScript 엔진V8, SpiderMonkey 등V8 엔진
이벤트 루프브라우저 내장libuv 라이브러리
API 제공자Web APIsNode.js APIs
스레드 풀브라우저 관리libuv 스레드 풀 (기본 4개)
실행 환경DOM, Window 객체Global, Process 객체

🔄 Node.js 이벤트 루프 페이즈

// Node.js 이벤트 루프의 6개 페이즈
┌───────────────────────────┐
┌─>│           timers          │  // setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  // I/O 콜백 지연 처리
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  // 내부적으로만 사용
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  // 새로운 I/O 이벤트 폴링
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  // setImmediate 콜백
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  // close 이벤트 콜백
   └───────────────────────────┘

📋 Node.js 이벤트 루프 구조 간략 정리

참고 출처:

🎯 핵심 특징
  1. libuv 기반: Node.js는 libuv 라이브러리를 통해 이벤트 루프 구현
  2. 6개 페이즈 순환: Timer → Pending Callbacks → Idle/Prepare → Poll → Check → Close Callbacks
  3. 각 페이즈별 큐: 각 단계마다 고유한 FIFO 큐 보유
  4. 시스템 실행 한도: 각 페이즈에서 큐를 모두 소진하거나 시스템 한도 도달 시 다음 페이즈로 이동
📊 페이즈별 역할
페이즈담당 작업큐 특성
TimerssetTimeout, setIntervalMin-Heap 우선순위 큐
Pending Callbacks이전 루프에서 지연된 I/O 콜백FIFO 큐
Idle, PrepareNode.js 내부 관리JavaScript 실행 없음
Poll새로운 I/O 이벤트, 거의 모든 콜백Watcher Queue
ChecksetImmediate 콜백 전용FIFO 큐
Close Callbackssocket.on('close') 등FIFO 큐
Poll 페이즈의 특별함
  • 대기 가능: 다른 페이즈와 달리 조건에 따라 대기 상태 유지
  • 핵심 역할: Node.js 비동기 I/O 모델의 중심
  • 블로킹 허용: 새로운 I/O 이벤트를 기다리며 효율적 리소스 사용
🔄 Thread Pool vs UV_IO
// Thread Pool 사용 (기본 4개 스레드)
fs.readFile('./file.txt', callback);     // 파일 I/O
crypto.pbkdf2('secret', 'salt', callback); // CPU 집약적 작업

// UV_IO 사용 (Kernel Thread 통신)
http.get('https://api.com', callback);   // 네트워크 I/O
db.query('SELECT * FROM users', callback); // 데이터베이스 I/O
🎯 면접 핵심 포인트
  • "Node.js는 정확히 어떤 부분이 싱글 스레드인가요?"
    → "JavaScript 실행(V8 엔진)은 싱글 스레드이지만, I/O 작업은 libuv의 스레드 풀과 커널 스레드를 활용하여 멀티스레딩으로 처리됩니다."

  • "이벤트 루프에서 가장 중요한 페이즈는 무엇인가요?"
    → "Poll 페이즈입니다. 새로운 I/O 이벤트를 폴링하고 대부분의 콜백을 실행하며, 필요시 대기하여 효율적인 리소스 사용을 담당합니다."

🎯 실행 순서 차이점 예시

브라우저 환경
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

// 출력: 1, 4, 3, 2
// Microtask(Promise)가 Macrotask(setTimeout)보다 우선
Node.js 환경
console.log('1');
setImmediate(() => console.log('2'));
setTimeout(() => console.log('3'), 0);
Promise.resolve().then(() => console.log('4'));
console.log('5');

// 출력: 1, 5, 4, 2, 3
// Node.js는 페이즈별로 처리하며, setImmediate는 check 페이즈에서 실행

🚀 Node.js 고유 API의 실행 순서

// process.nextTick과 setImmediate의 차이
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));

// 출력: nextTick, Promise, setImmediate
// process.nextTick이 가장 높은 우선순위를 가짐

🔀 멀티스레딩 처리 방식 차이

Node.js libuv 아키텍처
// Node.js는 I/O 작업을 커널 또는 스레드 풀로 위임
const fs = require('fs');
const crypto = require('crypto');

// 파일 I/O - 스레드 풀 사용
fs.readFile('large-file.txt', (err, data) => {
  console.log('파일 읽기 완료');
});

// CPU 집약적 작업 - 스레드 풀 사용 (CPU 집약적)
crypto.pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, key) => {
  console.log('암호화 완료');
});

// 네트워크 I/O - 이벤트 루프에서 처리
const https = require('https');
https.get('https://api.example.com', (res) => {
  console.log('HTTP 요청 완료');
});
브라우저 환경
// 브라우저는 Web APIs를 통해 비동기 처리
// DOM 이벤트
document.addEventListener('click', () => {
  console.log('클릭 이벤트');
});

// 네트워크 요청
fetch('https://api.example.com')
  .then(response => response.json())
  .then(data => console.log('Fetch 완료'));

// 타이머
setTimeout(() => console.log('Timer'), 1000);

4. Node.js SSL Handshake 이슈 발생 원인과 해결 방법

참고 출처: Step-by-Step Guide to Fixing Node.js SSL Certificate Errors

🚨 주요 SSL Handshake 에러 유형

1. UNABLE_TO_GET_ISSUER_CERT_LOCALLY
// 원인: 중간 인증서(Intermediate Certificate)가 누락되거나 
//       시스템의 신뢰할 수 있는 인증서 저장소에 루트 인증서가 없음

// ❌ 문제 상황
const https = require('https');
https.get('https://example.com', (res) => {
  // Error: unable to get local issuer certificate
});

// ✅ 해결 방법 1: 인증서 체인 완성
process.env.NODE_EXTRA_CA_CERTS = './path/to/intermediate-ca.pem';

// ✅ 해결 방법 2: 개발환경에서는 rejectUnauthorized: false를 사용하세요.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
2. DEPTH_ZERO_SELF_SIGNED_CERT
// 원인: 자체 서명 인증서(Self-signed Certificate) 사용

// ❌ 문제 상황
https.get('https://localhost:3000', callback); // 자체 서명 인증서 에러

// ✅ 해결 방법 1: 특정 자체 서명 인증서 신뢰
const selfSignedCert = fs.readFileSync('./self-signed-cert.pem');
const options = {
  hostname: 'localhost',
  port: 3000,
  rejectUnauthorized: false // 개발환경에서만 사용
};
3. CERT_HAS_EXPIRED
// 원인: 인증서 만료

// ✅ 해결 방법: 인증서 갱신 및 확인
const tls = require('tls');

function checkCertExpiry(hostname, port = 443) {
  return new Promise((resolve, reject) => {
    const socket = tls.connect(port, hostname, () => {
      const cert = socket.getPeerCertificate();
      const now = new Date();
      const expiry = new Date(cert.valid_to);
      
      console.log(`인증서 만료일: ${expiry}`);
      console.log(`현재 시간: ${now}`);
      console.log(`만료까지: ${Math.floor((expiry - now) / (1000 * 60 * 60 * 24))}`);
      
      socket.destroy();
      resolve(cert);
    });
    
    socket.on('error', reject);
  });
}

checkCertExpiry('example.com');
4. UNABLE_TO_VERIFY_LEAF_SIGNATURE
// 원인: 인증서 체인이 불완전하거나 손상됨

// ✅ 해결 방법: 완전한 인증서 체인 구성
// 올바른 인증서 순서: 서버 인증서 → 중간 인증서 → 루트 인증서
const fullChain = `
-----BEGIN CERTIFICATE-----
(서버 인증서)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(중간 인증서 1)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(중간 인증서 2 - 필요시)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(루트 인증서)
-----END CERTIFICATE-----
`;

fs.writeFileSync('./fullchain.pem', fullChain);
process.env.NODE_EXTRA_CA_CERTS = './fullchain.pem';
5. EPROTO: wrong version number
// 원인: TLS 버전 호환성 문제

// ✅ 해결 방법: TLS 버전 명시적 설정
const tls = require('tls');

const secureOptions = {
  hostname: 'old-server.example.com',
  port: 443,
  secureProtocol: 'TLSv1_2_method', // 특정 TLS 버전 강제
  // 또는 최소/최대 버전 설정
  minVersion: 'TLSv1.2',
  maxVersion: 'TLSv1.3'
};

🛠️ 실무 Best Practices

1. 환경별 인증서 관리
// config/ssl.js
const fs = require('fs');
const path = require('path');

const sslConfig = {
  development: {
    rejectUnauthorized: false, // 개발환경에서만
    ca: []
  },
  
  staging: {
    rejectUnauthorized: true,
    ca: [fs.readFileSync(path.join(__dirname, 'staging-ca.pem'))]
  },
  
  production: {
    rejectUnauthorized: true,
    ca: [
      fs.readFileSync(path.join(__dirname, 'prod-ca.pem')),
      fs.readFileSync(path.join(__dirname, 'intermediate-ca.pem'))
    ],
    secureProtocol: 'TLSv1_2_method'
  }
};

module.exports = sslConfig[process.env.NODE_ENV] || sslConfig.development;
2. SSL 상태 모니터링
// utils/ssl-monitor.js
const tls = require('tls');
const EventEmitter = require('events');

class SSLMonitor extends EventEmitter {
  constructor() {
    super();
    this.certificates = new Map();
  }
  
  async checkCertificate(hostname, port = 443) {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      
      const socket = tls.connect(port, hostname, {
        servername: hostname,
        timeout: 10000
      }, () => {
        const cert = socket.getPeerCertificate(true);
        const connectTime = Date.now() - startTime;
        
        const certInfo = {
          hostname,
          port,
          subject: cert.subject,
          issuer: cert.issuer,
          validFrom: new Date(cert.valid_from),
          validTo: new Date(cert.valid_to),
          fingerprint: cert.fingerprint,
          connectTime,
          checkedAt: new Date()
        };
        
        // 만료 임박 경고 (30일 전)
        const daysUntilExpiry = Math.floor(
          (certInfo.validTo - new Date()) / (1000 * 60 * 60 * 24)
        );
        
        if (daysUntilExpiry <= 30) {
          this.emit('certificateExpiring', {
            ...certInfo,
            daysUntilExpiry
          });
        }
        
        this.certificates.set(`${hostname}:${port}`, certInfo);
        socket.destroy();
        resolve(certInfo);
      });
      
      socket.on('error', reject);
    });
  }
  
  async checkMultipleSites(sites) {
    const results = await Promise.allSettled(
      sites.map(site => this.checkCertificate(site.hostname, site.port))
    );
    
    return results.map((result, index) => ({
      site: sites[index],
      status: result.status,
      data: result.status === 'fulfilled' ? result.value : result.reason
    }));
  }
}

// 사용 예시
const monitor = new SSLMonitor();

monitor.on('certificateExpiring', (cert) => {
  console.warn(`⚠️ 인증서 만료 임박: ${cert.hostname} (${cert.daysUntilExpiry}일 남음)`);
  // 슬랙, 이메일 등으로 알림 발송
});

monitor.on('sslError', (error) => {
  console.error(`❌ SSL 오류: ${error.hostname} - ${error.code}`);
});

// 정기적인 인증서 검사
setInterval(async () => {
  const sites = [
    { hostname: 'api.example.com', port: 443 },
    { hostname: 'admin.example.com', port: 443 }
  ];
  
  const results = await monitor.checkMultipleSites(sites);
  console.log('SSL 인증서 상태 검사 완료:', results.length);
}, 24 * 60 * 60 * 1000); // 24시간마다
3. Graceful SSL Error Handling
// utils/secure-request.js
const https = require('https');
const fs = require('fs');

class SecureHTTPSClient {
  constructor(options = {}) {
    this.defaultOptions = {
      timeout: 30000,
      retries: 3,
      retryDelay: 1000,
      ...options
    };
  }
  
  async request(url, options = {}) {
    const finalOptions = { ...this.defaultOptions, ...options };
    
    for (let attempt = 1; attempt <= finalOptions.retries; attempt++) {
      try {
        return await this._makeRequest(url, finalOptions);
      } catch (error) {
        if (attempt === finalOptions.retries) {
          throw this._enhanceError(error, url);
        }
        
        // SSL 관련 에러의 경우 재시도 간격 증가
        if (this._isSSLError(error)) {
          const delay = finalOptions.retryDelay * Math.pow(2, attempt - 1);
          await this._sleep(delay);
        }
      }
    }
  }
  
  _makeRequest(url, options) {
    return new Promise((resolve, reject) => {
      const req = https.request(url, options, (res) => {
        res.on('data', chunk => {
          // 응답 데이터 수집
        });
        res.on('end', () => {
          // 응답 종료 후 처리
        });
      });
      
      req.setTimeout(options.timeout, () => {
        req.destroy();
        reject(new Error('Request timeout'));
      });
      
      req.on('error', reject);
      req.end();
    });
  }
  
  _isSSLError(error) {
    const sslErrorCodes = [
      'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
      'DEPTH_ZERO_SELF_SIGNED_CERT',
      'CERT_HAS_EXPIRED',
      'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
      'EPROTO'
    ];
    
    return sslErrorCodes.includes(error.code);
  }
  
  _enhanceError(error, url) {
    const enhancedError = new Error(`SSL request failed for ${url}: ${error.message}`);
    enhancedError.originalError = error;
    enhancedError.code = error.code;
    enhancedError.url = url;
    
    // 에러별 해결책 제안
    enhancedError.solution = this._getSolution(error.code);
    
    return enhancedError;
  }
  
  _getSolution(errorCode) {
    const solutions = {
      'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': 
        'NODE_EXTRA_CA_CERTS 환경변수에 CA 인증서 경로를 설정하거나 ca 옵션을 사용하세요.',
      'DEPTH_ZERO_SELF_SIGNED_CERT': 
        '자체 서명 인증서를 ca 옵션에 추가하거나 개발환경에서는 rejectUnauthorized: false를 사용하세요.',
      'CERT_HAS_EXPIRED': 
        '인증서가 만료되었습니다. 새 인증서로 교체해주세요.',
      'UNABLE_TO_VERIFY_LEAF_SIGNATURE': 
        '완전한 인증서 체인을 구성하세요 (서버 → 중간 → 루트 인증서 순서).',
      'EPROTO': 
        'TLS 버전 호환성을 확인하고 secureProtocol 옵션을 설정하세요.'
    };
    
    return solutions[errorCode] || '공식 문서를 참조하여 SSL 설정을 확인하세요.';
  }
  
  _sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

module.exports = SecureHTTPSClient;

🎯 면접 답변 포인트

Q: Node.js에서 SSL handshake 에러가 발생했을 때 어떻게 해결하시나요?

A: "SSL handshake 에러는 주로 다음과 같은 원인으로 발생합니다:

  1. 인증서 체인 문제: UNABLE_TO_GET_ISSUER_CERT_LOCALLY 에러의 경우, NODE_EXTRA_CA_CERTS 환경변수로 중간 인증서를 지정하거나 요청 옵션에 ca 배열을 추가합니다.

  2. 자체 서명 인증서: DEPTH_ZERO_SELF_SIGNED_CERT 에러는 개발환경에서 흔하며, 해당 인증서를 ca 옵션에 명시적으로 추가하여 해결합니다.

  3. TLS 버전 호환성: EPROTO 에러의 경우 secureProtocol 또는 minVersion/maxVersion 옵션으로 TLS 버전을 명시합니다.

프로덕션에서는 절대 rejectUnauthorized: false를 사용하지 않으며, 환경별로 다른 SSL 설정을 관리하고 인증서 만료를 모니터링하는 시스템을 구축합니다."


💡 면접 대비 핵심 정리

Web Workers 관련 질문

  • Q: Web Workers의 제약사항은?
  • A: DOM 접근 불가, window 객체 사용 불가, 메모리 공유 불가 (SharedArrayBuffer 예외)

Service Workers 관련 질문

  • Q: Service Worker와 Web Worker의 차이점은?
  • A: Service Worker는 네트워크 프록시 역할, PWA 기능 제공, 브라우저가 생명주기 관리

Node.js Event Loop 관련 질문

  • Q: Node.js와 브라우저 이벤트 루프의 차이점은?
  • A: Node.js는 libuv 기반 6개 페이즈, process.nextTick 최고 우선순위, setImmediate vs setTimeout 순서 차이

SSL Handshake 관련 질문

  • Q: 프로덕션에서 SSL 에러 발생 시 대응 방안은?
  • A: 환경별 인증서 관리, 만료 모니터링, Graceful error handling, 절대 인증서 검증 비활성화 금지
profile
hi im gggongbo

0개의 댓글