PHpullh

TYPESCRIPT · 심층 가이드

TypeScript 성능 완전 정리

TypeScript 성능 최적화는 타입이 아니라 컴파일 이후 남는 JavaScript의 할당과 반복 비용을 다루는 일이며, 이 가이드는 문자열 버퍼링부터 청크 스트리밍까지 15가지 패턴을 측정 가능한 형태로 정리합니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

초심자가 가장 자주 오해하는 부분은 인터페이스나 제네릭이 실행 성능에 영향을 준다고 믿는 것입니다. 타입 주석은 컴파일 뒤 사라지므로 런타임 비용은 0이고, 실제로 비용을 만드는 것은 반복문 안의 객체 할당, 배열 복사, 문자열 연결, 그리고 매번 배열을 처음부터 훑는 조회입니다. 반대로 복잡한 조건부 타입이나 거대한 유니온이 만드는 비용은 빌드 시간과 에디터 응답 속도에 나타나며, 이는 런타임 최적화와는 원인도 해결책도 완전히 다른 문제입니다.

시간 측정을 먼저 익혀 기준선을 만드는 순서를 권합니다. 측정 없이 고치면 무엇이 나아졌는지 알 수 없기 때문입니다. 그다음 조회 맵 사용과 Set 조회 최적화로 선형 탐색을 상수 시간 조회로 바꾸고, 같은 입력이 반복될 때는 메모이제이션 캐시로 계산 자체를 건너뜁니다. 데이터가 메모리보다 클 때는 청크 스트리밍과 배치 I/O로 처리 단위를 나눕니다.

덜 알려진 함정은 메모이제이션 캐시가 사실상 메모리 누수 장치가 되는 경우입니다. 상한이나 만료 정책이 없는 Map은 프로세스가 사는 동안 계속 자라고, 키로 객체를 쓰면 참조가 다르다는 이유로 캐시가 매번 빗나가면서 항목만 쌓입니다. 마찬가지로 루프 안에서 전개 연산자로 배열을 누적하면 매 반복마다 전체 복사가 일어나 입력이 두 배가 될 때 시간이 네 배로 늘어납니다.

01문자열 버퍼링

문자열 배열을 만들어 두고 마지막에 join("-")으로 한 번에 이어 붙입니다. 반복문 안에서 +=로 계속 붙이는 대신 조각을 모아 두는 방식입니다. 요소가 문자열이 아니어도 join은 각 항목을 문자열로 바꿔 넣는데, null과 undefined는 "null"이 아니라 빈 문자열로 처리된다는 차이가 있습니다.

TypeScript code

// 문자열 버퍼링
const parts: string[] = ["a", "b", "1"];
console.log(parts.join("-"));
알아두면 좋은 점

join의 기본 구분자는 쉼표라, 인자 없이 부르면 쉼표로 이어집니다. 붙여 쓰려면 join('')을 명시해야 합니다.

자주 하는 실수

배열에 null이나 undefined가 섞이면 빈 문자열로 바뀌어, 값이 빠진 자리가 구분자만 연달아 나오는 형태로 남습니다.

02사전 할당

fib는 이미 계산한 값을 Map에 넣어 두고 다음 호출에서 재활용하므로, 같은 인자에 대한 재귀가 반복되지 않습니다. 메모이제이션 없이 두 번 재귀하면 호출 수가 지수적으로 늘어나는 문제를 이 한 줄이 없앱니다. 배열을 미리 잡아 두는 방식과 비교하면 필요한 항목만 저장하지만, 항목마다 해시 조회 비용이 붙습니다.

TypeScript code

// 사전 할당
const cache = new Map();
const fib = (x: number) => { if (x <= 1) return x; if (cache.has(x)) return cache.get(x); const value = fib(x - 1) + fib(x - 2); cache.set(x, value); return value; };
console.log(fib(8));
알아두면 좋은 점

재귀 메모이제이션은 호출 횟수를 지수에서 선형으로 줄여 주지만, 결과를 담을 사전이 인자 종류만큼 커진다는 대가가 따릅니다.

자주 하는 실수

타입 인자 없는 Map은 get이 any를 돌려주어, 값이 없을 때의 undefined가 검사 없이 덧셈에 들어가 NaN이 됩니다.

03지연 처리

Set에 세 개의 태그를 미리 넣고 has("ts")로 포함 여부를 확인합니다. 확인이 여러 번 반복되는 자리라면 배열 대신 이 형태가 유리합니다. 다만 Set은 첫 조회 시점까지 생성 비용이 들기 때문에, 원소가 몇 개뿐이고 한 번만 확인한다면 includes가 더 빠를 수 있습니다.

TypeScript code

// 지연 처리
const tags = new Set(["js", "ts", "rust"]);
console.log(tags.has("ts"));
알아두면 좋은 점

Set은 값 비교에 SameValueZero를 쓰므로 NaN도 하나의 값으로 취급되어, 배열 기반 검사와 결과가 달라질 수 있습니다.

자주 하는 실수

함수 안에서 호출마다 new Set(arr)을 만들면, 조회를 빠르게 하려던 이득이 생성 비용에 그대로 상쇄됩니다.

04메모이제이션 캐시

performance.now()로 시작 시각을 재고 reduce로 배열 합계를 구한 뒤 소요 시간을 함께 출력합니다. 어떤 계산이 얼마나 걸리는지 감을 잡을 때 쓰는 최소 형태입니다. 이 정도로 짧은 작업은 JIT 최적화와 타이머 해상도의 영향을 크게 받아, 한 번 측정한 값만으로 성능을 판단하면 안 됩니다.

TypeScript code

// 메모이제이션 캐시
const start = performance.now();
const total = [1, 2, 3, 4, 4].reduce((sum, value) => sum + value, 0);
console.log(total, performance.now() - start);
알아두면 좋은 점

측정값의 편차를 줄이려면 같은 작업을 여러 번 반복한 뒤 평균이 아니라 중앙값을 보는 편이 이상치에 훨씬 덜 흔들립니다.

자주 하는 실수

한 번 잰 값으로 성능을 판단하면 최적화 준비 단계와 타이머 해상도가 그대로 섞여, 실제와 정반대인 결론을 내리게 됩니다.

05조회 맵 사용

조회용 자료구조 대신 문자열 조립이 나옵니다. 조각을 string[]에 담아 두었다가 join으로 한 번에 합칩니다. 이 방식의 이점은 중간 문자열을 만들지 않는다는 것인데, 실제로 최신 JS 엔진은 연속 문자열 결합도 내부적으로 최적화하므로 차이는 조각 수가 아주 많을 때 두드러집니다.

TypeScript code

// 조회 맵 사용
const parts: string[] = ["a", "b", "5"];
console.log(parts.join("-"));
알아두면 좋은 점

조각을 배열로 모아 두면 최종 길이를 미리 알 수 있어, 로그 한 줄의 크기 상한을 두는 처리도 함께 붙이기 쉽습니다.

자주 하는 실수

루프를 돌 때마다 parts.join('-')을 부르면 매번 전체를 다시 이어 붙이므로, 조각 수가 늘수록 비용이 제곱으로 커집니다.

06배치 I/O

묶음 I/O 대신 계산 결과를 모아 두는 코드입니다. cache.has(x)로 저장 여부를 먼저 확인하고, 없을 때만 재귀 계산 후 set합니다. 이 확인-계산-저장 순서는 배치 처리에도 그대로 쓰이지만, 캐시가 비동기 호출을 감쌀 경우 같은 키에 대한 동시 요청이 중복 실행된다는 함정이 있습니다.

TypeScript code

// 배치 I/O
const cache = new Map();
const fib = (x: number) => { if (x <= 1) return x; if (cache.has(x)) return cache.get(x); const value = fib(x - 1) + fib(x - 2); cache.set(x, value); return value; };
console.log(fib(8));
알아두면 좋은 점

확인·계산·저장 순서를 지키면 같은 키의 중복 계산을 막을 수 있고, 계산 부분을 인자로 빼면 다른 작업에도 그대로 재사용됩니다.

자주 하는 실수

비동기 호출을 감싸면서 완료된 값만 저장하면, 첫 호출이 끝나기 전에 들어온 요청들이 모두 별도로 실행되어 캐시가 무의미해집니다.

07불필요한 복사 줄이기

복사를 줄이는 예시로 보기에는, 이 코드가 Set을 한 번 만들어 두고 has로 조회만 합니다. 컬렉션을 매번 새로 만들지 않고 재사용한다는 점이 요지입니다. 참고로 new Set(arr)는 원본 배열을 복사해 새 컨테이너를 만들므로, 루프 안에서 이 변환을 반복하면 절약하려던 비용을 되레 더 씁니다.

TypeScript code

// 불필요한 복사 줄이기
const tags = new Set(["js", "ts", "rust"]);
console.log(tags.has("ts"));
알아두면 좋은 점

같은 집합을 여러 번 조회한다면 함수 밖에서 한 번 만들어 두고 참조만 넘기는 편이, 호출마다 만드는 것보다 확실히 저렴합니다.

자주 하는 실수

루프 안에서 집합을 새로 만들면 매 회차 배열 전체를 복사해, 절약하려던 조회 비용보다 훨씬 큰 비용을 쓰게 됩니다.

08시간 측정

측정 구간은 start 기록과 performance.now() 재호출 사이이며, 그 안에서 다섯 개짜리 배열을 reduce로 더합니다. 시간과 결과를 같이 찍어 두면 최적화가 계산을 망가뜨리지 않았는지 확인하기 좋습니다. Date.now()와 달리 performance.now()는 밀리초 미만 소수까지 제공하고 시스템 시계 변경의 영향을 받지 않습니다.

TypeScript code

// 시간 측정
const start = performance.now();
const total = [1, 2, 3, 4, 8].reduce((sum, value) => sum + value, 0);
console.log(total, performance.now() - start);
알아두면 좋은 점

performance.now()는 시스템 시계 변경에 영향받지 않는 단조 증가 값이라, 구간 측정에는 Date.now()보다 적합합니다.

자주 하는 실수

측정 구간 안에 출력 호출을 넣으면 그 비용까지 함께 재게 되어, 실제 계산 시간보다 훨씬 큰 값이 나옵니다.

09병렬 처리

병렬 실행은 없고, 배열 요소를 순차적으로 이어 붙이는 join 호출뿐입니다. 순서가 결과에 그대로 반영되는 작업이라 병렬화 대상도 아닙니다. 실제로 JS는 단일 스레드로 돌기 때문에 이런 CPU 계산은 Promise.all로 감싸도 빨라지지 않고, 워커를 써야 실제 병렬이 됩니다.

TypeScript code

// 병렬 처리
const parts: string[] = ["a", "b", "9"];
console.log(parts.join("-"));
알아두면 좋은 점

CPU를 쓰는 계산을 진짜로 병렬화하려면 node:worker_threads 같은 별도 스레드가 필요하고, 데이터 전달 비용도 함께 따져야 합니다.

자주 하는 실수

동기 계산을 Promise.all로 감싸면 실행 시점만 뒤로 밀릴 뿐 총 시간은 그대로이고, 프로미스 생성 비용만 늘어납니다.

10객체 재사용

매 호출마다 값을 새로 만들지 않고 Map에 저장된 것을 다시 쓰는 구조입니다. 같은 입력이 자주 반복되는 함수에 붙이면 계산량이 크게 줄어듭니다. 대신 이 캐시는 비우는 코드가 없어 호출한 키 종류만큼 계속 커지므로, 입력 범위가 넓은 함수에 그대로 쓰면 메모리가 단조 증가합니다.

TypeScript code

// 객체 재사용
const cache = new Map();
const fib = (x: number) => { if (x <= 1) return x; if (cache.has(x)) return cache.get(x); const value = fib(x - 1) + fib(x - 2); cache.set(x, value); return value; };
console.log(fib(8));
알아두면 좋은 점

캐시 크기에 상한을 두고 오래된 항목부터 버리면, 적중률을 크게 잃지 않으면서 메모리 사용량을 예측 가능한 범위에 묶을 수 있습니다.

자주 하는 실수

비우는 코드가 없는 캐시는 호출된 키 종류만큼 계속 커져, 입력 범위가 넓은 함수에서는 메모리가 단조 증가합니다.

11Set 조회 최적화

Set.has는 요소 수가 늘어도 조회 시간이 거의 일정한 반면, 배열의 includes는 앞에서부터 훑기 때문에 길이에 비례해 느려집니다. 중복 없는 목록에 포함 여부를 자주 물어야 한다면 이 코드처럼 Set으로 만들어 두는 편이 낫습니다. 다만 Set은 인덱스 접근과 정렬된 순회를 제공하지 않습니다.

TypeScript code

// Set 조회 최적화
const tags = new Set(["js", "ts", "rust"]);
console.log(tags.has("ts"));
알아두면 좋은 점

인덱스 접근이나 정렬된 순회가 함께 필요하면 집합과 배열을 나란히 두고, 조회는 집합에 순회는 배열에 맡기는 방식도 흔합니다.

자주 하는 실수

요소가 서너 개뿐인 목록까지 집합으로 바꾸면 생성과 해시 비용이 붙어, 단순한 includes보다 오히려 느려지는 경우가 있습니다.

12청크 스트리밍

청크 단위 스트리밍 대신, 배열 전체를 reduce로 한 번에 접으며 걸린 시간을 출력합니다. 데이터가 메모리에 다 들어오는 크기라면 이렇게 통째로 처리하는 편이 단순합니다. 반대로 입력이 커지면 전체를 배열로 올리는 순간 메모리가 정점을 찍으므로, 그때부터 조각 단위 처리가 필요해집니다.

TypeScript code

// 청크 스트리밍
const start = performance.now();
const total = [1, 2, 3, 4, 12].reduce((sum, value) => sum + value, 0);
console.log(total, performance.now() - start);
알아두면 좋은 점

입력을 조각 단위로 처리하면 메모리 사용량이 조각 크기에 머물러, 파일이 커져도 같은 코드로 버틸 수 있습니다.

자주 하는 실수

전체를 배열로 올린 뒤 접는 코드는 입력이 커지는 순간 메모리 정점이 그대로 올라가, 어느 크기부터 갑자기 실패하기 시작합니다.

13정렬 비용 비교

정렬은 등장하지 않고 join으로 배열을 문자열로 합칩니다. 원소 순서를 그대로 유지한 채 출력 형태만 만드는 작업입니다. 정렬을 붙일 때 알아 둘 점은 Array.prototype.sort가 인자 없이 호출되면 원소를 문자열로 변환해 비교한다는 것으로, 숫자 배열에서는 비교 함수를 반드시 넘겨야 합니다.

TypeScript code

// 정렬 비용 비교
const parts: string[] = ["a", "b", "13"];
console.log(parts.join("-"));
알아두면 좋은 점

정렬은 비교 횟수가 요소 수에 로그를 곱한 만큼 늘어나므로, 비교 함수 안에서 무거운 계산을 하면 그 비용이 그대로 곱해집니다.

자주 하는 실수

숫자 배열에 비교 함수 없이 sort()를 쓰면 문자열로 변환해 비교하므로 [1, 13, 2] 같은 순서가 나옵니다.

14소유권 또는 참조 재사용

값을 새로 만드는 대신 cache에 담아 둔 참조를 계속 돌려주는 코드입니다. 재계산 비용이 큰 결과를 여러 곳에서 공유할 때 이 형태가 유효합니다. 주의할 것은 반환되는 값이 복사본이 아니라 저장된 그 객체라는 점으로, 호출한 쪽에서 내용을 바꾸면 다음 호출자에게도 바뀐 상태가 그대로 보입니다.

TypeScript code

// 소유권 또는 참조 재사용
const cache = new Map();
const fib = (x: number) => { if (x <= 1) return x; if (cache.has(x)) return cache.get(x); const value = fib(x - 1) + fib(x - 2); cache.set(x, value); return value; };
console.log(fib(8));
알아두면 좋은 점

저장한 값을 Object.freeze로 얼려 두면 엄격 모드에서 수정 시도가 예외로 드러나, 공유 참조가 조용히 바뀌는 일을 막을 수 있습니다.

자주 하는 실수

캐시가 돌려주는 것은 복사본이 아니라 저장된 그 객체이므로, 호출부가 필드를 하나 바꾸면 이후 모든 호출자가 바뀐 값을 받습니다.

15페이지네이션 슬라이스

잘라 내는 코드는 없고 Set에 담긴 태그의 포함 여부만 확인합니다. 필터 조건 판정을 미리 만든 집합에 맡기는 형태입니다. 목록을 페이지로 자를 때 쓰는 slice는 원본을 건드리지 않고 새 배열을 만드는 반면 splice는 원본을 잘라 내므로, 이름이 비슷한 두 메서드를 혼동하지 않아야 합니다.

TypeScript code

// 페이지네이션 슬라이스
const tags = new Set(["js", "ts", "rust"]);
console.log(tags.has("ts"));
알아두면 좋은 점

slice는 음수 인덱스를 뒤에서부터 세므로, 마지막 몇 개를 꺼낼 때 길이를 계산하지 않고 slice(-n)으로 끝낼 수 있습니다.

자주 하는 실수

splice는 원본을 잘라 내고 제거된 요소를 돌려주므로, slice인 줄 알고 쓰면 페이지를 넘길 때마다 원본이 줄어듭니다.

정리하며

  • 고치기 전에 측정하고, 같은 입력과 같은 조건에서 전후를 비교합니다
  • 반복 조회가 있으면 배열 탐색을 Map이나 Set 조회로 바꿉니다
  • 메모이제이션 캐시에는 반드시 크기 상한이나 만료 정책을 함께 둡니다
  • 루프 안 전개 연산자 누적을 push나 사전 할당 배열로 교체합니다

더 깊이 들어가고 싶다면 TypeScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.