TypeScript 주니어 성능 최적화 기술면접
새 면접Q. TypeScript로 작성된 Node.js 애플리케이션에서 메모리 사용량이 지속적으로 증가하여 결국 서버가 다운되는 메모리 누수(Memory Leak) 문제가 발생했습니다. 메모리 누수의 주요 원인과 이를 진단하기 위한 방법, 그리고 TypeScript 코드 레벨에서 예방할 수 있는 방법을 설명해주세요.
이벤트 리스너, 타이머, 클로저, 전역 변수 등이 메모리를 계속 참조하고 있는 상황을 생각해보세요.
메모리 누수의 주요 원인은 제거되지 않은 이벤트 리스너, clearInterval/clearTimeout이 호출되지 않은 타이머, 클로저에 의한 불필요한 참조 유지, 전역 변수에 계속 데이터가 쌓이는 경우 등입니다. 진단 방법으로는 Node.js의 --inspect 플래그와 Chrome DevTools의 Memory Profiler를 사용하거나, process.memoryUsage()로 힙 메모리를 모니터링할 수 있습니다. 예방을 위해서는 이벤트 리스너를 등록할 때 반드시 removeEventListener로 제거하고, 타이머 사용 후 clear 함수를 호출하며, WeakMap이나 WeakSet을 사용해 약한 참조를 유지하고, 큰 객체는 사용 후 명시적으로 null을 할당하는 것이 좋습니다. TypeScript에서는 인터페이스로 cleanup 메서드를 강제하거나, Disposable 패턴을 구현하여 리소스 해제를 보장할 수 있습니다.
- • 이벤트 리스너와 타이머 미제거가 주요 원인
- • Chrome DevTools Memory Profiler로 진단 가능
- • WeakMap/WeakSet 사용으로 약한 참조 유지
- • Disposable 패턴으로 리소스 해제 보장
실시간 채팅 서버에서 WebSocket 연결이 끊어진 후에도 이벤트 리스너가 남아있어 메모리가 계속 증가하는 문제를 해결할 때 사용됩니다.
WeakMap과 일반 Map의 차이점은 무엇이며, 어떤 상황에서 WeakMap을 사용해야 메모리 누수를 방지할 수 있나요?
Q. TypeScript 기반 웹 애플리케이션의 초기 로딩 시간이 느려 사용자 이탈이 발생하고 있습니다. 번들 크기를 분석한 결과 JavaScript 파일이 3MB 이상으로 너무 큽니다. 번들 크기를 줄이고 초기 로딩 성능을 개선하기 위한 방법들을 설명해주세요.
코드 분할, 트리 쉐이킹, 동적 import, 라이브러리 선택 등을 고려해보세요.
번들 크기를 줄이기 위해서는 먼저 webpack-bundle-analyzer 같은 도구로 번들 구성을 분석하여 큰 용량을 차지하는 라이브러리를 파악해야 합니다. Code Splitting을 통해 라우트별로 번들을 분리하고 dynamic import를 사용해 필요한 시점에만 코드를 로드합니다. Tree Shaking이 제대로 동작하도록 ES6 모듈 형식을 사용하고, 사용하지 않는 코드를 제거합니다. moment.js 대신 date-fns나 day.js처럼 가벼운 라이브러리로 대체하고, lodash 전체 대신 필요한 함수만 import합니다. tsconfig.json에서 target을 최신 브라우저에 맞게 설정하여 불필요한 polyfill을 줄이고, production 빌드 시 minification과 compression을 적용합니다.
- • webpack-bundle-analyzer로 번들 구성 분석
- • Code Splitting과 dynamic import로 지연 로딩
- • Tree Shaking을 위한 ES6 모듈 사용
- • 무거운 라이브러리를 가벼운 대안으로 교체
대시보드 애플리케이션에서 차트 라이브러리를 해당 페이지 진입 시에만 로드하여 초기 번들 크기를 줄일 때 사용됩니다.
dynamic import를 사용할 때 TypeScript에서 타입 안정성을 유지하는 방법은 무엇인가요?
Q. React와 TypeScript로 개발한 대시보드에서 1000개 이상의 데이터를 테이블로 렌더링할 때 스크롤이 버벅거리고 입력 필드의 타이핑이 느려지는 문제가 발생했습니다. 이런 대량 데이터 렌더링 시 성능을 개선하기 위한 방법들을 설명해주세요.
가상화(Virtualization), 메모이제이션, 디바운싱 등의 기법을 고려해보세요.
대량 데이터 렌더링 성능 개선을 위해서는 먼저 react-window나 react-virtualized 같은 가상 스크롤 라이브러리를 사용하여 실제로 화면에 보이는 행만 렌더링해야 합니다. React.memo로 불필요한 재렌더링을 방지하고, useMemo와 useCallback으로 계산 비용이 큰 값과 함수를 메모이제이션합니다. 검색이나 필터링 기능은 debounce나 throttle을 적용하여 과도한 상태 업데이트를 방지합니다. 테이블 행 컴포넌트를 작게 분리하고 각 행이 독립적으로 업데이트되도록 구조화합니다. 또한 서버 사이드 페이지네이션을 도입하여 클라이언트에서 한 번에 렌더링하는 데이터 양 자체를 줄이는 것도 효과적입니다.
- • 가상 스크롤로 보이는 영역만 렌더링
- • React.memo와 useMemo로 메모이제이션
- • debounce/throttle로 과도한 업데이트 방지
- • 서버 사이드 페이지네이션 도입
관리자 페이지에서 수천 개의 주문 내역을 테이블로 보여줄 때 스크롤 성능을 개선하기 위해 사용됩니다.
React.memo의 두 번째 인자로 커스텀 비교 함수를 전달할 때 TypeScript에서 어떻게 타입을 정의하나요?
Q. TypeScript와 TypeORM을 사용하는 프로젝트에서 사용자 목록 조회 API의 응답 시간이 5초 이상 걸립니다. 쿼리를 확인한 결과 N+1 문제가 발생하고 있습니다. N+1 문제가 무엇인지 설명하고, TypeORM에서 이를 해결하는 방법을 제시해주세요.
연관된 엔티티를 조회할 때 각 레코드마다 추가 쿼리가 발생하는 상황을 생각해보세요.
N+1 문제는 메인 쿼리로 N개의 레코드를 조회한 후, 각 레코드의 연관 데이터를 가져오기 위해 N번의 추가 쿼리가 발생하는 문제입니다. 예를 들어 100명의 사용자를 조회하면서 각 사용자의 프로필 정보를 별도로 가져온다면 총 101번의 쿼리가 실행됩니다. TypeORM에서는 relations 옵션이나 leftJoinAndSelect를 사용하여 Eager Loading을 구현해 한 번의 쿼리로 연관 데이터를 함께 가져올 수 있습니다. QueryBuilder를 사용할 때는 leftJoinAndSelect 메서드로 JOIN을 명시적으로 작성합니다. 또한 find 메서드의 relations 배열에 필요한 관계를 명시하면 자동으로 JOIN 쿼리가 생성됩니다. 성능 개선을 위해서는 필요한 관계만 선택적으로 로드하고, select를 사용해 필요한 컬럼만 조회하는 것이 좋습니다.
- • N+1은 연관 데이터 조회 시 추가 쿼리가 N번 발생하는 문제
- • relations 옵션이나 leftJoinAndSelect로 Eager Loading
- • 한 번의 JOIN 쿼리로 연관 데이터를 함께 조회
- • 필요한 관계와 컬럼만 선택적으로 로드
게시글 목록과 각 게시글의 작성자 정보를 함께 조회할 때 N+1 문제를 해결하여 응답 시간을 단축할 때 사용됩니다.
Eager Loading과 Lazy Loading의 차이점은 무엇이며, 각각 어떤 상황에서 사용하는 것이 적합한가요?
Q. REST API에서 응답 데이터의 크기가 너무 커서 네트워크 전송 시간이 오래 걸립니다. TypeScript 백엔드에서 API 응답 크기를 줄이고 전송 속도를 개선하기 위한 방법들을 설명해주세요.
압축, 필요한 필드만 선택, 페이지네이션 등을 고려해보세요.
API 응답 크기를 줄이기 위해서는 먼저 gzip이나 brotli 같은 압축 알고리즘을 적용하여 전송 데이터를 압축해야 합니다. Express에서는 compression 미들웨어를 사용하면 자동으로 응답을 압축할 수 있습니다. 클라이언트가 필요한 필드만 선택적으로 요청할 수 있도록 GraphQL을 도입하거나, REST API에서도 쿼리 파라미터로 fields를 받아 필요한 속성만 반환하도록 구현합니다. 대량의 데이터는 페이지네이션을 적용하여 한 번에 전송하는 양을 제한하고, 이미지나 파일 URL은 CDN 링크로 제공하여 실제 바이너리 데이터는 응답에 포함하지 않습니다. TypeScript에서는 DTO(Data Transfer Object)를 정의하여 필요한 필드만 포함된 타입을 명시적으로 관리할 수 있습니다.
- • gzip/brotli 압축으로 전송 데이터 크기 감소
- • 필요한 필드만 선택적으로 반환
- • 페이지네이션으로 한 번에 전송하는 양 제한
- • DTO로 응답 구조 명시적 관리
모바일 앱에서 느린 네트워크 환경의 사용자를 위해 API 응답 크기를 최소화하여 로딩 속도를 개선할 때 사용됩니다.
TypeScript에서 DTO를 정의할 때 class-transformer와 class-validator를 함께 사용하는 이유는 무엇인가요?
Q. TypeScript로 작성된 배치 작업에서 10,000개의 이메일을 외부 API를 통해 발송해야 합니다. 순차적으로 처리하면 너무 오래 걸리고, 모두 동시에 처리하면 API Rate Limit에 걸립니다. 효율적으로 대량의 비동기 작업을 처리하는 방법을 설명해주세요.
동시 실행 개수를 제한하는 방법과 배치 처리를 고려해보세요.
대량의 비동기 작업을 효율적으로 처리하기 위해서는 동시 실행 개수를 제한하는 Concurrency Control이 필요합니다. p-limit 같은 라이브러리를 사용하거나 직접 구현하여 한 번에 실행되는 Promise 개수를 제한할 수 있습니다. 예를 들어 동시에 최대 10개씩만 처리하도록 설정하면 API Rate Limit을 초과하지 않으면서도 순차 처리보다 빠릅니다. 배열을 일정 크기의 청크로 나누어 각 청크를 순차적으로 처리하되 청크 내에서는 병렬로 처리하는 방식도 효과적입니다. 실패한 요청은 재시도 로직을 구현하되, exponential backoff를 적용하여 일시적인 오류에 대응합니다. TypeScript에서는 제네릭을 활용하여 재사용 가능한 배치 처리 유틸리티 함수를 타입 안전하게 구현할 수 있습니다.
- • 동시 실행 개수를 제한하는 Concurrency Control
- • 배열을 청크로 나누어 배치 처리
- • 실패 시 exponential backoff로 재시도
- • 제네릭으로 재사용 가능한 유틸리티 구현
대량의 사용자에게 푸시 알림을 발송하거나 수천 개의 이미지를 외부 API로 처리할 때 사용됩니다.
Promise.all을 사용할 때와 for...of with await를 사용할 때의 성능 차이는 무엇인가요?
Q. 사용자 활동 로그를 저장하는 테이블에 수백만 개의 레코드가 쌓여 있습니다. 특정 사용자의 최근 활동을 조회하는 쿼리가 매우 느립니다. 이 쿼리를 최적화하기 위해 어떤 인덱스를 생성해야 하며, TypeScript 코드에서 쿼리를 작성할 때 인덱스가 제대로 사용되도록 하려면 어떻게 해야 하나요?
복합 인덱스의 컬럼 순서와 쿼리 조건의 관계를 생각해보세요.
특정 사용자의 최근 활동을 조회하려면 user_id와 created_at 컬럼에 복합 인덱스를 생성해야 하며, 인덱스 컬럼 순서는 WHERE 절에 사용되는 user_id를 먼저, ORDER BY에 사용되는 created_at을 나중에 배치합니다. 복합 인덱스는 (user_id, created_at) 순서로 생성하면 특정 사용자를 빠르게 찾고 그 사용자의 레코드를 시간순으로 정렬할 수 있습니다. TypeORM에서는 @Index 데코레이터로 엔티티에 인덱스를 정의하거나 마이그레이션에서 createIndex를 사용합니다. 쿼리 작성 시 WHERE user_id = ? ORDER BY created_at DESC 형태로 작성해야 인덱스가 효과적으로 사용되며, EXPLAIN 명령어로 실행 계획을 확인하여 인덱스가 실제로 사용되는지 검증해야 합니다. 인덱스 컬럼 순서가 잘못되면 인덱스를 타지 않거나 부분적으로만 사용될 수 있습니다.
- • WHERE와 ORDER BY에 사용되는 컬럼에 복합 인덱스 생성
- • 인덱스 컬럼 순서는 WHERE 절 컬럼을 먼저 배치
- • @Index 데코레이터나 마이그레이션으로 인덱스 정의
- • EXPLAIN으로 실행 계획 확인하여 검증
사용자별 주문 내역을 최신순으로 조회하는 API에서 응답 시간을 개선할 때 사용됩니다.
인덱스를 너무 많이 생성하면 어떤 문제가 발생할 수 있나요?
Q. 이커머스 상품 목록 페이지에서 각 상품의 썸네일 이미지를 표시합니다. 페이지 로딩 시 모든 이미지가 한 번에 로드되어 초기 로딩이 느리고 불필요한 네트워크 대역폭을 사용합니다. TypeScript와 React를 사용하여 이미지 로딩을 최적화하는 방법을 설명해주세요.
Lazy Loading, 이미지 포맷, 반응형 이미지 등을 고려해보세요.
이미지 로딩 최적화를 위해서는 먼저 Intersection Observer API를 사용한 Lazy Loading을 구현하여 뷰포트에 보이는 이미지만 로드해야 합니다. img 태그의 loading='lazy' 속성을 사용하면 브라우저 네이티브 Lazy Loading을 활용할 수 있습니다. 이미지 포맷은 WebP나 AVIF 같은 최신 포맷을 사용하되 picture 태그로 fallback을 제공하고, srcset과 sizes 속성으로 반응형 이미지를 구현하여 디바이스에 맞는 크기를 로드합니다. 썸네일은 원본보다 작은 크기로 리사이징된 이미지를 CDN에서 제공하고, 중요한 상단 이미지는 preload 링크 태그로 우선 로드합니다. TypeScript에서는 이미지 URL과 크기 정보를 타입으로 정의하여 올바른 이미지가 로드되도록 보장할 수 있습니다.
- • Intersection Observer나 loading='lazy'로 Lazy Loading
- • WebP/AVIF 포맷 사용과 picture 태그로 fallback
- • srcset/sizes로 반응형 이미지 구현
- • CDN에서 리사이징된 썸네일 제공
쇼핑몰 상품 목록에서 수백 개의 상품 이미지를 효율적으로 로드하여 페이지 성능을 개선할 때 사용됩니다.
Next.js의 Image 컴포넌트는 어떤 최적화 기능을 자동으로 제공하나요?
Q. TypeScript와 Redux를 사용하는 대규모 애플리케이션에서 전역 상태가 업데이트될 때마다 관련 없는 컴포넌트들까지 모두 재렌더링되어 성능 문제가 발생합니다. Redux 상태 관리에서 불필요한 재렌더링을 방지하는 방법을 설명해주세요.
셀렉터 최적화, 상태 정규화, 구독 범위 제한 등을 고려해보세요.
Redux에서 불필요한 재렌더링을 방지하려면 먼저 useSelector를 사용할 때 필요한 상태만 선택하고, Reselect 라이브러리의 createSelector로 메모이제이션된 셀렉터를 만들어야 합니다. 상태 구조를 정규화(normalization)하여 중첩된 객체 대신 평탄한 구조로 관리하면 특정 엔티티 업데이트 시 영향 범위를 최소화할 수 있습니다. shallowEqual 같은 커스텀 비교 함수를 useSelector의 두 번째 인자로 전달하여 얕은 비교로 변경을 감지합니다. 큰 리스트는 React.memo로 개별 아이템 컴포넌트를 메모이제이션하고, Redux Toolkit의 createSlice를 사용하면 Immer가 내장되어 불변성을 쉽게 유지할 수 있습니다. TypeScript에서는 RootState 타입을 정의하여 셀렉터의 타입 안정성을 보장하고, 제네릭으로 재사용 가능한 셀렉터를 만들 수 있습니다.
- • createSelector로 메모이제이션된 셀렉터 생성
- • 상태 정규화로 업데이트 영향 범위 최소화
- • shallowEqual로 얕은 비교 적용
- • React.memo로 컴포넌트 메모이제이션
실시간 대시보드에서 특정 위젯의 데이터만 업데이트되었을 때 다른 위젯들이 재렌더링되지 않도록 최적화할 때 사용됩니다.
Redux Toolkit의 createEntityAdapter는 어떤 방식으로 상태 정규화를 도와주나요?
Q. TypeScript로 작성된 Node.js API 서버가 갑작스러운 트래픽 증가로 응답 시간이 느려지고 일부 요청이 타임아웃됩니다. 서버의 병목 지점을 찾고 부하를 처리하기 위한 방법들을 설명해주세요.
모니터링, 로드 밸런싱, 수평 확장, 비동기 처리 등을 고려해보세요.
서버 부하 문제를 해결하려면 먼저 New Relic이나 PM2 모니터링으로 CPU, 메모리, 이벤트 루프 지연 시간을 측정하여 병목 지점을 파악해야 합니다. 동기적으로 처리되는 무거운 작업(이미지 처리, 파일 압축 등)이 있다면 Worker Threads나 별도의 작업 큐(Bull, BullMQ)로 분리하여 메인 스레드를 차단하지 않도록 합니다. Nginx나 AWS ALB 같은 로드 밸런서를 도입하여 여러 서버 인스턴스로 트래픽을 분산하고, PM2 클러스터 모드로 CPU 코어 수만큼 프로세스를 실행하여 수평 확장합니다. 데이터베이스 커넥션 풀 크기를 적절히 설정하고, 자주 조회되는 데이터는 Redis 같은 인메모리 캐시에 저장합니다. TypeScript에서는 타입을 활용하여 큐 작업 페이로드와 결과를 안전하게 정의하고, 헬스 체크 엔드포인트를 구현하여 로드 밸런서가 서버 상태를 모니터링할 수 있게 합니다.
- • 모니터링 도구로 CPU, 메모리, 이벤트 루프 측정
- • Worker Threads나 작업 큐로 무거운 작업 분리
- • 로드 밸런서와 클러스터 모드로 수평 확장
- • Redis 캐시와 커넥션 풀 최적화
이벤트 티켓 오픈 시 수만 명이 동시 접속하여 서버 부하가 급증할 때 안정적으로 서비스를 제공하기 위해 사용됩니다.
PM2 클러스터 모드에서 여러 프로세스 간에 상태를 공유해야 할 때 어떤 방법을 사용할 수 있나요?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!