[Career] 면접 기술 질문 정리 - JavaScript와 알고리즘 기초
면접 기술 질문 정리 - JavaScript와 알고리즘 기초
오늘 면접을 봤는데, 기술 질문에서 제대로 대답을 못했습니다. 요즘 AI를 활용해서 개발을 진행하다 보니 코드를 직접 작성할 일이 거의 없어서 기본기가 많이 부족했었네요... 면접에서 나온 질문들을 다시 한번 정리하고 공부한 내용을 포스팅합니다.
📚 면접에서 나온 질문들
- 쓰로틀(Throttle)과 디바운스(Debounce)
- 스택(Stack)과 힙(Heap) 구조
- DFS와 BFS의 차이
- 백트래킹(Backtracking)
- 얕은 복사와 깊은 복사 (JavaScript)
- 함수형 프로그래밍
- 원시형 타입과 참조형 타입
- 제너릭(Generic)
1. 쓰로틀(Throttle)과 디바운스(Debounce)
쓰로틀이란?
쓰로틀은 일정 시간 동안 함수가 최대 한 번만 실행되도록 제한하는 기법입니다. 스크롤 이벤트나 리사이즈 이벤트처럼 빈번하게 발생하는 이벤트를 제어할 때 사용합니다.
핵심 개념:
- 일정 시간 간격으로 함수 실행
- 마지막 호출 후 일정 시간이 지나야 다시 실행 가능
- 연속된 호출 중 일부만 실행
javascript// 쓰로틀 구현 예제 function throttle(func, delay) { let lastCall = 0; // 마지막 호출 시간 return function (...args) { const now = Date.now(); // 마지막 호출로부터 delay 시간이 지났는지 확인 if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } }; } // 사용 예제: 스크롤 이벤트에 쓰로틀 적용 const handleScroll = throttle(() => { console.log('스크롤 이벤트 발생'); }, 1000); // 1초마다 최대 한 번만 실행 window.addEventListener('scroll', handleScroll);
디바운스란?
디바운스는 연속된 함수 호출 중 마지막 호출만 실행하도록 하는 기법입니다. 검색 입력창에서 사용자가 타이핑을 멈춘 후에만 API를 호출하는 경우에 사용합니다.
핵심 개념:
- 연속된 호출을 무시하고 마지막 호출만 실행
- 일정 시간 동안 새로운 호출이 없을 때만 실행
- 타이머를 리셋하면서 대기
javascript// 디바운스 구현 예제 function debounce(func, delay) { let timeoutId; // 타이머 ID return function (...args) { // 이전 타이머가 있으면 취소 clearTimeout(timeoutId); // 새로운 타이머 설정 timeoutId = setTimeout(() => { func.apply(this, args); }, delay); }; } // 사용 예제: 검색 입력창에 디바운스 적용 const handleSearch = debounce((query) => { console.log('검색어:', query); // API 호출 등 }, 500); // 0.5초 동안 입력이 없을 때만 실행 input.addEventListener('input', (e) => { handleSearch(e.target.value); });
쓰로틀 vs 디바운스 차이점
| 특징 | 쓰로틀 | 디바운스 |
|---|---|---|
| 실행 시점 | 일정 시간 간격으로 실행 | 마지막 호출 후 일정 시간 대기 |
| 사용 사례 | 스크롤, 리사이즈 이벤트 | 검색 입력, 버튼 클릭 방지 |
| 실행 빈도 | 정기적으로 실행 | 마지막에 한 번만 실행 |
실무 활용:
- 쓰로틀: 무한 스크롤, 스크롤 위치 저장, 리사이즈 이벤트 처리
- 디바운스: 검색 자동완성, 폼 유효성 검사, 버튼 중복 클릭 방지
2. 스택(Stack)과 힙(Heap) 구조
스택(Stack)이란?
스택은 LIFO(Last In First Out) 구조로, 나중에 들어온 데이터가 먼저 나가는 자료구조입니다. JavaScript에서 함수 호출, 변수 저장 등에 사용됩니다.
핵심 특징:
- 후입선출(LIFO) 구조
- 메모리에 순차적으로 저장
- 빠른 접근 속도
- 크기가 제한적
javascript// 스택 구조 예제 class Stack { constructor() { this.items = []; } // 데이터 추가 (push) push(element) { this.items.push(element); } // 데이터 제거 (pop) pop() { if (this.items.length === 0) { return '스택이 비어있습니다'; } return this.items.pop(); } // 최상단 요소 확인 (peek) peek() { return this.items[this.items.length - 1]; } // 스택이 비어있는지 확인 isEmpty() { return this.items.length === 0; } } // 사용 예제 const stack = new Stack(); stack.push(1); stack.push(2); stack.push(3); console.log(stack.pop()); // 3 (마지막에 들어온 것) console.log(stack.pop()); // 2 console.log(stack.pop()); // 1
힙(Heap)이란?
힙은 완전 이진 트리 기반의 자료구조로, 우선순위 큐를 구현하는 데 사용됩니다. 최소 힙과 최대 힙이 있습니다.
핵심 특징:
- 완전 이진 트리 구조
- 부모 노드가 자식 노드보다 항상 크거나 작음 (힙 속성)
- 최소 힙: 부모 ≤ 자식
- 최대 힙: 부모 ≥ 자식
javascript// 최소 힙 구현 예제 class MinHeap { constructor() { this.heap = []; } // 부모 노드 인덱스 getParentIndex(index) { return Math.floor((index - 1) / 2); } // 왼쪽 자식 노드 인덱스 getLeftChildIndex(index) { return 2 * index + 1; } // 오른쪽 자식 노드 인덱스 getRightChildIndex(index) { return 2 * index + 2; } // 데이터 추가 insert(value) { this.heap.push(value); this.heapifyUp(); } // 힙 속성 유지 (위로) heapifyUp() { let index = this.heap.length - 1; while (index > 0) { const parentIndex = this.getParentIndex(index); if (this.heap[parentIndex] <= this.heap[index]) { break; } // 부모와 자식 교환 [this.heap[parentIndex], this.heap[index]] = [ this.heap[index], this.heap[parentIndex] ]; index = parentIndex; } } // 최소값 제거 extractMin() { if (this.heap.length === 0) { return null; } const min = this.heap[0]; const last = this.heap.pop(); if (this.heap.length > 0) { this.heap[0] = last; this.heapifyDown(); } return min; } // 힙 속성 유지 (아래로) heapifyDown() { let index = 0; while (this.getLeftChildIndex(index) < this.heap.length) { let smallerChildIndex = this.getLeftChildIndex(index); const rightChildIndex = this.getRightChildIndex(index); if ( rightChildIndex < this.heap.length && this.heap[rightChildIndex] < this.heap[smallerChildIndex] ) { smallerChildIndex = rightChildIndex; } if (this.heap[index] <= this.heap[smallerChildIndex]) { break; } [this.heap[index], this.heap[smallerChildIndex]] = [ this.heap[smallerChildIndex], this.heap[index] ]; index = smallerChildIndex; } } } // 사용 예제 const heap = new MinHeap(); heap.insert(5); heap.insert(3); heap.insert(8); heap.insert(1); console.log(heap.extractMin()); // 1 (최소값) console.log(heap.extractMin()); // 3
스택 vs 힙 차이점
| 특징 | 스택 | 힙 |
|---|---|---|
| 구조 | LIFO (후입선출) | 완전 이진 트리 |
| 접근 | 순차적, 빠름 | 트리 탐색 필요 |
| 용도 | 함수 호출, 변수 저장 | 우선순위 큐, 정렬 |
| 복잡도 | O(1) | 삽입/삭제 O(log n) |
3. DFS와 BFS의 차이
DFS (Depth-First Search) - 깊이 우선 탐색
DFS는 깊이를 우선으로 탐색하는 알고리즘입니다. 스택이나 재귀를 사용하여 구현합니다.
핵심 특징:
- 한 경로를 끝까지 탐색한 후 다음 경로로 이동
- 스택 또는 재귀 사용
- 메모리 사용량이 적음
- 최단 경로를 보장하지 않음
javascript// DFS 재귀 구현 function dfsRecursive(graph, start, visited = new Set()) { visited.add(start); console.log(start); // 방문한 노드 출력 // 인접한 노드들을 탐색 for (const neighbor of graph[start]) { if (!visited.has(neighbor)) { dfsRecursive(graph, neighbor, visited); } } return visited; } // DFS 스택 구현 function dfsStack(graph, start) { const stack = [start]; const visited = new Set(); while (stack.length > 0) { const node = stack.pop(); // 스택에서 제거 if (!visited.has(node)) { visited.add(node); console.log(node); // 방문한 노드 출력 // 인접한 노드들을 스택에 추가 (역순으로 추가하여 순서 유지) for (let i = graph[node].length - 1; i >= 0; i--) { const neighbor = graph[node][i]; if (!visited.has(neighbor)) { stack.push(neighbor); } } } } return visited; } // 그래프 예제 const graph = { A: ['B', 'C'], B: ['A', 'D', 'E'], C: ['A', 'F'], D: ['B'], E: ['B', 'F'], F: ['C', 'E'] }; console.log('DFS 재귀:', dfsRecursive(graph, 'A')); // 출력: A -> B -> D -> E -> F -> C console.log('DFS 스택:', dfsStack(graph, 'A')); // 출력: A -> C -> F -> E -> B -> D
BFS (Breadth-First Search) - 너비 우선 탐색
BFS는 너비를 우선으로 탐색하는 알고리즘입니다. 큐를 사용하여 구현합니다.
핵심 특징:
- 같은 레벨의 노드를 모두 탐색한 후 다음 레벨로 이동
- 큐 사용
- 최단 경로를 보장
- 메모리 사용량이 많을 수 있음
javascript// BFS 큐 구현 function bfs(graph, start) { const queue = [start]; const visited = new Set([start]); while (queue.length > 0) { const node = queue.shift(); // 큐에서 제거 console.log(node); // 방문한 노드 출력 // 인접한 노드들을 큐에 추가 for (const neighbor of graph[node]) { if (!visited.has(neighbor)) { visited.add(neighbor); queue.push(neighbor); } } } return visited; } // 그래프 예제 const graph = { A: ['B', 'C'], B: ['A', 'D', 'E'], C: ['A', 'F'], D: ['B'], E: ['B', 'F'], F: ['C', 'E'] }; console.log('BFS:', bfs(graph, 'A')); // 출력: A -> B -> C -> D -> E -> F
DFS vs BFS 차이점
| 특징 | DFS | BFS |
|---|---|---|
| 탐색 방식 | 깊이 우선 | 너비 우선 |
| 자료구조 | 스택 또는 재귀 | 큐 |
| 메모리 | 적음 (깊이만큼) | 많음 (너비만큼) |
| 최단 경로 | 보장하지 않음 | 보장함 |
| 사용 사례 | 미로 탐색, 백트래킹 | 최단 경로, 레벨 순회 |
실무 활용:
- DFS: 파일 시스템 탐색, 미로 찾기, 사이클 감지
- BFS: 최단 경로 찾기, 소셜 네트워크 탐색, 레벨 순회
4. 백트래킹(Backtracking)
백트래킹은 문제를 해결하기 위해 가능한 모든 경우를 시도하되, 조건을 만족하지 않으면 이전 단계로 돌아가는 알고리즘입니다.
핵심 개념:
- 가능한 모든 경우를 탐색
- 조건을 만족하지 않으면 되돌아감 (백트랙)
- 재귀를 사용하여 구현
- 가지치기(Pruning)로 불필요한 탐색 제거
javascript// N-Queen 문제 예제 function solveNQueens(n) { const board = Array(n) .fill(null) .map(() => Array(n).fill('.')); const result = []; // 퀸을 배치할 수 있는지 확인 function isValid(row, col) { // 같은 열에 퀸이 있는지 확인 for (let i = 0; i < row; i++) { if (board[i][col] === 'Q') { return false; } } // 왼쪽 위 대각선 확인 for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { if (board[i][j] === 'Q') { return false; } } // 오른쪽 위 대각선 확인 for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { if (board[i][j] === 'Q') { return false; } } return true; } // 백트래킹 함수 function backtrack(row) { // 모든 행에 퀸을 배치했으면 결과에 추가 if (row === n) { result.push(board.map((row) => row.join(''))); return; } // 각 열에 퀸을 배치 시도 for (let col = 0; col < n; col++) { if (isValid(row, col)) { board[row][col] = 'Q'; // 퀸 배치 backtrack(row + 1); // 다음 행으로 board[row][col] = '.'; // 백트랙 (되돌리기) } } } backtrack(0); return result; } // 사용 예제 console.log(solveNQueens(4)); // 출력: [['.Q..', '...Q', 'Q...', '..Q.'], ['..Q.', 'Q...', '...Q', '.Q..']]
백트래킹 패턴:
- 선택: 현재 상태에서 가능한 선택을 시도
- 재귀: 다음 단계로 진행
- 되돌리기: 조건을 만족하지 않으면 이전 상태로 복원
실무 활용:
- N-Queen 문제
- 스도쿠 풀이
- 조합/순열 생성
- 미로 찾기
5. 얕은 복사와 깊은 복사 (JavaScript)
얕은 복사(Shallow Copy)
얕은 복사는 객체의 최상위 레벨만 복사하고, 중첩된 객체는 참조를 공유합니다.
javascript// 얕은 복사 방법들 // 1. Spread 연산자 const original = { a: 1, b: { c: 2 } }; const shallow1 = { ...original }; // 2. Object.assign() const shallow2 = Object.assign({}, original); // 3. Array.slice() (배열의 경우) const arr = [1, 2, [3, 4]]; const shallowArr = arr.slice(); // 문제: 중첩된 객체는 참조를 공유 shallow1.b.c = 999; console.log(original.b.c); // 999 (원본도 변경됨!)
깊은 복사(Deep Copy)
깊은 복사는 객체의 모든 레벨을 복사하여 완전히 독립적인 복사본을 만듭니다.
javascript// 깊은 복사 방법들 // 1. JSON.parse(JSON.stringify()) - 간단하지만 제한적 const original = { a: 1, b: { c: 2 }, d: [3, 4] }; const deep1 = JSON.parse(JSON.stringify(original)); deep1.b.c = 999; console.log(original.b.c); // 2 (원본은 변경되지 않음) // 주의: 함수, undefined, Symbol 등은 복사되지 않음 const objWithFunc = { a: 1, fn: () => console.log('test') }; const deep2 = JSON.parse(JSON.stringify(objWithFunc)); console.log(deep2.fn); // undefined // 2. 재귀 함수로 구현 function deepCopy(obj) { // null이거나 원시 타입이면 그대로 반환 if (obj === null || typeof obj !== 'object') { return obj; } // Date 객체 처리 if (obj instanceof Date) { return new Date(obj.getTime()); } // 배열 처리 if (Array.isArray(obj)) { return obj.map((item) => deepCopy(item)); } // 객체 처리 const copied = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { copied[key] = deepCopy(obj[key]); } } return copied; } // 사용 예제 const original = { a: 1, b: { c: 2, d: [3, 4] }, e: new Date() }; const deep = deepCopy(original); deep.b.c = 999; deep.b.d.push(5); console.log(original.b.c); // 2 (원본은 변경되지 않음) console.log(original.b.d); // [3, 4] (원본은 변경되지 않음)
얕은 복사 vs 깊은 복사 차이점
| 특징 | 얕은 복사 | 깊은 복사 |
|---|---|---|
| 복사 범위 | 최상위 레벨만 | 모든 레벨 |
| 중첩 객체 | 참조 공유 | 독립적인 복사본 |
| 성능 | 빠름 | 느림 |
| 메모리 | 적음 | 많음 |
실무 활용:
- 얕은 복사: 단순한 객체, 성능이 중요한 경우
- 깊은 복사: 중첩된 객체, 원본 보호가 필요한 경우
6. 함수형 프로그래밍
함수형 프로그래밍은 함수를 일급 객체로 취급하고, 부수 효과(side effect)를 최소화하는 프로그래밍 패러다임입니다.
핵심 개념
1. 순수 함수(Pure Function)
- 같은 입력에 항상 같은 출력
- 부수 효과가 없음
javascript// 순수 함수 예제 function add(a, b) { return a + b; // 부수 효과 없음, 항상 같은 결과 } // 비순수 함수 예제 let count = 0; function increment() { count++; // 외부 상태를 변경 (부수 효과) return count; }
2. 불변성(Immutability)
- 데이터를 변경하지 않고 새로운 데이터를 생성
javascript// 가변적 (Mutable) const arr = [1, 2, 3]; arr.push(4); // 원본 배열 변경 // 불변적 (Immutable) const arr = [1, 2, 3]; const newArr = [...arr, 4]; // 새로운 배열 생성
3. 고차 함수(Higher-Order Function)
- 함수를 인자로 받거나 함수를 반환하는 함수
javascript// map, filter, reduce 등 const numbers = [1, 2, 3, 4, 5]; // map: 각 요소를 변환 const doubled = numbers.map((n) => n * 2); // [2, 4, 6, 8, 10] // filter: 조건에 맞는 요소만 필터링 const evens = numbers.filter((n) => n % 2 === 0); // [2, 4] // reduce: 배열을 하나의 값으로 축소 const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
4. 함수 합성(Function Composition)
- 여러 함수를 조합하여 새로운 함수 생성
javascript// 함수 합성 예제 const add = (x) => x + 1; const multiply = (x) => x * 2; const subtract = (x) => x - 3; // 함수 합성 const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x); const composed = compose(subtract, multiply, add); console.log(composed(5)); // ((5 + 1) * 2) - 3 = 9
함수형 프로그래밍의 장점
- 예측 가능성: 순수 함수로 인한 예측 가능한 동작
- 테스트 용이성: 부수 효과가 없어 테스트하기 쉬움
- 재사용성: 함수를 조합하여 재사용
- 병렬 처리: 부수 효과가 없어 병렬 처리에 안전
7. 원시형 타입과 참조형 타입
원시형 타입(Primitive Type)
원시형 타입은 값을 직접 저장하는 타입입니다. JavaScript에서 원시형 타입은 불변(immutable)입니다.
원시형 타입 종류:
number,string,boolean,null,undefined,symbol,bigint
javascript// 원시형 타입 예제 let a = 10; let b = a; // 값 복사 b = 20; console.log(a); // 10 (원본은 변경되지 않음) console.log(b); // 20 // 문자열도 원시형 타입 let str1 = 'hello'; let str2 = str1; // 값 복사 str2 = 'world'; console.log(str1); // 'hello' (원본은 변경되지 않음) console.log(str2); // 'world'
참조형 타입(Reference Type)
참조형 타입은 메모리 주소를 저장하는 타입입니다. 객체, 배열, 함수 등이 해당됩니다.
참조형 타입 종류:
object,array,function,Date,RegExp등
javascript// 참조형 타입 예제 let obj1 = { a: 1, b: 2 }; let obj2 = obj1; // 참조 복사 (주소만 복사) obj2.a = 999; console.log(obj1.a); // 999 (원본도 변경됨!) console.log(obj2.a); // 999 // 배열도 참조형 타입 let arr1 = [1, 2, 3]; let arr2 = arr1; // 참조 복사 arr2.push(4); console.log(arr1); // [1, 2, 3, 4] (원본도 변경됨!) console.log(arr2); // [1, 2, 3, 4]
원시형 vs 참조형 차이점
| 특징 | 원시형 타입 | 참조형 타입 |
|---|---|---|
| 저장 방식 | 값을 직접 저장 | 메모리 주소 저장 |
| 복사 방식 | 값 복사 | 참조 복사 |
| 변경 가능성 | 불변 (immutable) | 가변 (mutable) |
| 비교 | 값으로 비교 | 참조로 비교 |
javascript// 비교 예제 // 원시형: 값으로 비교 const a = 10; const b = 10; console.log(a === b); // true (값이 같음) // 참조형: 참조로 비교 const obj1 = { a: 1 }; const obj2 = { a: 1 }; console.log(obj1 === obj2); // false (다른 참조) // 같은 참조인 경우 const obj3 = obj1; console.log(obj1 === obj3); // true (같은 참조)
8. 제너릭(Generic)
제너릭은 타입을 파라미터로 받아서 재사용 가능한 코드를 작성하는 기법입니다. TypeScript에서 주로 사용됩니다.
제너릭이란?
제너릭을 사용하면 타입에 구애받지 않고 재사용 가능한 함수나 클래스를 만들 수 있습니다.
typescript// 제너릭 함수 예제 function identity<T>(arg: T): T { return arg; } // 사용 예제 const number = identity<number>(10); // number 타입 const string = identity<string>('hello'); // string 타입 const boolean = identity<boolean>(true); // boolean 타입 // 타입 추론도 가능 const inferred = identity(10); // 자동으로 number 타입으로 추론
제너릭을 사용하는 이유
1. 타입 안정성
- 컴파일 타임에 타입 체크 가능
- 런타임 에러 방지
2. 코드 재사용성
- 여러 타입에 대해 동일한 로직 사용 가능
typescript// 제너릭 없이 작성하면... function getFirstNumber(arr: number[]): number { return arr[0]; } function getFirstString(arr: string[]): string { return arr[0]; } // 제너릭으로 작성하면... function getFirst<T>(arr: T[]): T { return arr[0]; } // 사용 const firstNumber = getFirst<number>([1, 2, 3]); // 1 const firstString = getFirst<string>(['a', 'b', 'c']); // 'a'
제너릭 제약 조건
제너릭에 제약 조건을 추가하여 특정 조건을 만족하는 타입만 사용하도록 할 수 있습니다.
typescript// 제약 조건 예제: length 속성이 있는 타입만 허용 interface Lengthwise { length: number; } function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); // length 속성 사용 가능 return arg; } logLength('hello'); // OK (string은 length 속성 있음) logLength([1, 2, 3]); // OK (array는 length 속성 있음) // logLength(10); // Error (number는 length 속성 없음)
제너릭 클래스
클래스에도 제너릭을 사용할 수 있습니다.
typescript// 제너릭 클래스 예제 class Box<T> { private value: T; constructor(value: T) { this.value = value; } getValue(): T { return this.value; } setValue(value: T): void { this.value = value; } } // 사용 예제 const numberBox = new Box<number>(10); console.log(numberBox.getValue()); // 10 const stringBox = new Box<string>('hello'); console.log(stringBox.getValue()); // 'hello'
실무 활용
제너릭은 React에서도 자주 사용됩니다.
typescript// React에서 제너릭 사용 예제 interface User { id: number; name: string; } function useFetch<T>(url: string): [T | null, boolean, Error | null] { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { fetch(url) .then((res) => res.json()) .then((data: T) => { setData(data); setLoading(false); }) .catch((err) => { setError(err); setLoading(false); }); }, [url]); return [data, loading, error]; } // 사용 const [user, loading, error] = useFetch<User>('/api/user');
정리
면접에서 나온 기술 질문들을 다시 정리하면서, 제가 부족했던 부분들을 명확히 이해할 수 있었습니다. 특히 AI를 활용한 개발로 인해 기본기가 부족했던 것 같아서, 이번 기회에 다시 한번 공부할 수 있어서 좋았습니다.
핵심 포인트:
- 쓰로틀/디바운스: 이벤트 최적화에 필수적인 개념
- 스택/힙: 자료구조의 기본, 메모리 관리 이해에 중요
- DFS/BFS: 그래프 탐색 알고리즘의 기본
- 백트래킹: 모든 경우를 탐색하는 문제 해결 기법
- 얕은/깊은 복사: JavaScript에서 객체 다룰 때 중요한 개념
- 함수형 프로그래밍: 코드의 예측 가능성과 재사용성 향상
- 원시형/참조형: 메모리 관리와 변수 동작 이해의 핵심
- 제너릭: 타입 안정성과 코드 재사용성을 위한 TypeScript 기능
앞으로는 AI를 활용하더라도 기본기를 꾸준히 공부하고, 직접 코드를 작성해보는 습관을 기르려고 합니다. 면접 준비를 하면서 다시 한번 기본의 중요성을 느꼈습니다.