PHpullh

TYPESCRIPT · 심층 가이드

TypeScript 함수 완전 정리

기본 함수와 기본값 매개변수, 클로저, 고차 함수, 제네릭, 메모이제이션, 파이프라인까지 TypeScript 함수 20가지 형태를 정리했습니다.

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

함수는 TypeScript에서 타입이 가장 크게 힘을 발휘하는 자리입니다. 매개변수와 반환값에 타입을 붙이는 순간, 호출하는 쪽의 실수가 실행 전에 걸러지고 콜백의 인자 타입까지 추론이 흘러갑니다. 반대로 타입을 any로 열어 두면 그 지점부터 추론이 끊겨 아래쪽 전체가 검사 밖으로 빠져나갑니다.

기본 함수 선언과 람다 함수를 나란히 보며 두 표기의 차이를 확인하는 것으로 시작하십시오. 기본값 매개변수와 가변 인자 합계가 호출 형태를 넓혀 주고, 클로저 카운터는 상태를 함수 안에 가두는 방식을 보여 줍니다. 뒤쪽의 제네릭 함수와 고차 함수 map, compose 함수는 한 묶음으로 읽어야 타입이 어떻게 이어져 흐르는지가 보입니다.

화살표 함수와 일반 함수의 차이는 문법이 아니라 this에 있습니다. 일반 함수는 호출 방식에 따라 this가 바뀌지만 화살표 함수는 정의된 자리의 this를 그대로 씁니다. 클래스 메서드를 콜백으로 넘겼을 때 this가 사라지는 고전적인 버그가 여기서 나옵니다. 콜백으로 건네질 함수라면 화살표 형태로 정의하거나 바인딩을 명시하십시오.

01기본 함수 선언

add는 매개변수 두 개와 반환값에 모두 number를 붙인 가장 기본적인 함수 선언입니다. 인자 타입을 적어 두면 호출부에서 문자열을 넘겼을 때 실행 전에 잡힙니다. 반환 타입은 추론으로도 채워지지만, 명시해 두면 구현을 잘못 고쳤을 때 오류가 호출부가 아닌 함수 안에서 발생합니다.

TypeScript code

// 기본 함수 선언
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(1, 2));
알아두면 좋은 점

매개변수 이름은 호출부의 자동 완성과 문서에 그대로 드러나므로, 의미 있는 이름을 쓰면 인자 순서를 헷갈릴 일이 줄어듭니다.

자주 하는 실수

인자가 둘 다 number라 자리를 바꿔도 컴파일이 통과하므로, 의미가 다른 값이라면 타입만으로는 순서 실수를 막을 수 없습니다.

02두 값을 더하는 함수

나머지 매개변수 ...values로 받은 숫자들을 reduce로 접어 총합을 냅니다. 인자 개수가 정해지지 않은 유틸리티를 만들 때 배열을 따로 감싸지 않아도 되는 방식입니다. values는 언제나 number[]이며 인자를 하나도 안 넘기면 빈 배열이 되어, 초깃값 0 덕분에 예외 없이 0이 나옵니다.

TypeScript code

// 두 값을 더하는 함수
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 2));
알아두면 좋은 점

나머지 매개변수는 반드시 마지막에 와야 하고, 이미 배열로 가진 값을 넘길 때는 sum(...values)처럼 펼쳐서 전달해야 합니다.

자주 하는 실수

요소가 아주 많은 배열을 펼쳐 넘기면 인자 개수 한도에 걸려 RangeError가 날 수 있으므로, 큰 목록은 배열 자체를 받는 형태가 안전합니다.

03기본값 매개변수

makeCounter가 지역 변수 current를 감싼 함수를 돌려주고, 반환된 next를 두 번 부르면 1과 2가 나옵니다. 외부에 노출하지 않을 상태를 함수 안에 가둘 때 쓰는 구조입니다. console.log(next(), next())는 인자를 왼쪽부터 평가하므로 출력 순서가 호출 순서와 일치합니다.

TypeScript code

// 기본값 매개변수
function makeCounter() {
  let current: number = 0;
  return () => ++current;
}
const next = makeCounter();
console.log(next(), next());
알아두면 좋은 점

console.log(next(), next())는 인자를 왼쪽부터 평가하도록 언어가 보장하므로, 출력이 1과 2 순서로 나오는 것은 우연이 아닙니다.

자주 하는 실수

반환된 next를 여러 곳에서 공유하면 하나의 current를 함께 늘리므로, 독립된 번호가 필요한 곳마다 새로 만들어야 합니다.

04가변 인자 합계

제네릭 first<T>가 배열의 0번 원소를 꺼내고, 문자열 배열을 넘겼으므로 결과 타입은 string | undefined로 좁혀집니다. 어떤 타입의 목록이든 첫 값을 안전하게 꺼내야 할 때 이 서명이 적합합니다. 반환 타입에 undefined를 넣어 둔 덕분에 빈 배열 상황을 호출부가 반드시 처리하게 됩니다.

TypeScript code

// 가변 인자 합계
function first<T>(values: T[]): T | undefined {
  return values[0];
}
console.log(first(["a", "b"]));
알아두면 좋은 점

T는 인자에서 추론되므로 대부분 명시할 필요가 없고, 빈 배열 리터럴을 넘기면 T가 never로 잡혀 이후 사용이 막힙니다.

자주 하는 실수

noUncheckedIndexedAccess를 켜지 않으면 values[0]이 T로 보이므로, 반환 타입에 undefined를 직접 적지 않으면 빈 배열 상황이 숨습니다.

05객체 또는 구조체 반환

객체를 만들어 돌려주는 대신 두 수의 합이라는 원시값 하나만 반환합니다. 반환할 정보가 여럿으로 늘어나면 이 자리에 객체 리터럴이 들어가고 타입도 함께 커집니다. 여러 값을 담은 객체를 반환할 때는 인터페이스를 따로 선언해 두어야 호출부에서 필드 이름 오타가 걸러집니다.

TypeScript code

// 객체 또는 구조체 반환
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(5, 2));
알아두면 좋은 점

돌려줄 값이 여럿이면 튜플보다 이름 붙은 객체가 낫습니다. 호출부에서 구조 분해할 때 순서를 기억하지 않아도 되기 때문입니다.

자주 하는 실수

객체 리터럴을 그대로 반환하면 타입이 자동 추론되어, 필드를 하나 빠뜨려도 정의부는 통과하고 호출부에서만 오류가 납니다.

06람다 함수

람다 대신 function 선언을 쓰되, 내부 reduce 콜백은 화살표 함수로 작성한 혼합 형태입니다. 짧은 콜백은 화살표로, 이름이 필요한 최상위 함수는 선언문으로 두는 조합이 읽기 편합니다. 두 문법의 실질적 차이는 길이가 아니라 this 처리와 호이스팅 여부에 있습니다.

TypeScript code

// 람다 함수
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 6));
알아두면 좋은 점

화살표 함수는 arguments 객체를 갖지 않으므로, 전달된 인자 전체를 다루려면 나머지 매개변수를 명시해야 합니다.

자주 하는 실수

메서드 안 콜백을 일반 function으로 쓰면 this가 달라져, 인스턴스 필드 접근이 undefined가 되거나 예외로 이어집니다.

07클로저 카운터

카운터 상태가 makeCounter 호출마다 새로 만들어지며, next는 그 하나를 계속 증가시킵니다. 인스턴스마다 독립된 번호가 필요할 때 클래스 없이 해결하는 방법입니다. makeCounter()를 한 번 더 호출하면 완전히 다른 current가 생겨 번호가 1부터 다시 시작합니다.

TypeScript code

// 클로저 카운터
function makeCounter() {
  let current: number = 0;
  return () => ++current;
}
const next = makeCounter();
console.log(next(), next());
알아두면 좋은 점

클로저에 감춘 상태는 밖에서 접근할 경로 자체가 없어, # 필드처럼 실행 시점까지 확실히 보호됩니다.

자주 하는 실수

루프 안에서 만든 클로저가 바깥 변수를 공유하면 모든 콜백이 같은 값을 보게 되므로, 반복마다 새 스코프가 생기는지 확인해야 합니다.

08재귀 팩토리얼

재귀 대신, 배열에서 첫 원소를 한 번 꺼내는 제네릭 함수가 정의돼 있습니다. 타입 매개변수 T가 인자에서 string으로 추론되어 별도 지정 없이 호출됩니다. 인덱스 접근은 범위를 벗어나도 예외 없이 undefined를 주므로, 기본 설정에서는 반환 타입에 이를 직접 적어야 합니다.

TypeScript code

// 재귀 팩토리얼
function first<T>(values: T[]): T | undefined {
  return values[0];
}
console.log(first(["a", "b"]));
알아두면 좋은 점

재귀 함수는 반환 타입을 명시해 두는 편이 좋습니다. 자기 자신을 참조하는 추론은 컴파일러가 포기하고 오류를 낼 수 있기 때문입니다.

자주 하는 실수

인덱스 접근은 범위를 벗어나도 예외 없이 undefined를 주므로, 종료 조건을 값의 존재가 아니라 길이 비교로 두지 않으면 재귀가 끝나지 않습니다.

09고차 함수 map

map에 넘길 만한 형태이긴 하지만 여기서는 add(9, 2)를 직접 호출해 결과만 찍습니다. 고차 함수에 넘길 콜백도 먼저 이렇게 단독으로 검증해 두면 문제 범위를 좁히기 쉽습니다. add를 그대로 map에 넘기면 두 번째 인자로 인덱스가 들어가 의도와 다른 값이 나옵니다.

TypeScript code

// 고차 함수 map
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(9, 2));
알아두면 좋은 점

콜백을 넘기기 전에 단독으로 호출해 결과를 확인해 두면, 문제가 콜백에 있는지 순회 쪽에 있는지 범위를 바로 좁힐 수 있습니다.

자주 하는 실수

arr.map(add)로 넘기면 두 번째 인자에 인덱스가 들어가, 의도한 두 값의 합 대신 값과 인덱스의 합이 나옵니다.

10조건 함수 predicate

참·거짓을 돌려주는 술어가 아니라 숫자 여러 개를 합산하는 함수입니다. 조건 검사도 같은 골격을 쓰되 reduce 대신 every나 some으로 바꾸면 됩니다. 누적 콜백의 total 타입은 초깃값에서 추론되므로 0 대신 null을 넣으면 이후 덧셈에서 타입 오류가 납니다.

TypeScript code

// 조건 함수 predicate
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 10));
알아두면 좋은 점

every와 some은 결과가 정해지는 순간 순회를 멈추므로, 조건 판정에는 끝까지 도는 reduce보다 유리합니다.

자주 하는 실수

초깃값을 null로 주면 누산기 타입이 그대로 추론돼 이후 total + value가 컴파일 오류가 납니다.

11정렬용 비교 함수

정렬 비교자와는 별개로, 호출할 때마다 값이 1씩 커지는 클로저를 만들어 두 번 실행합니다. 정렬 과정에서 비교 횟수를 세는 계측기를 붙일 때 이런 카운터가 그대로 쓰입니다. ++current는 증가 후 값을 돌려주므로 첫 호출이 0이 아니라 1을 반환합니다.

TypeScript code

// 정렬용 비교 함수
function makeCounter() {
  let current: number = 0;
  return () => ++current;
}
const next = makeCounter();
console.log(next(), next());
알아두면 좋은 점

++current는 증가 후 값을, current++는 증가 전 값을 돌려주므로 첫 호출 결과가 1과 0으로 갈립니다.

자주 하는 실수

비교 함수 안에 카운터 같은 부수 효과를 두면 호출 횟수가 엔진의 정렬 구현에 따라 달라져, 그 값을 검사하는 테스트가 환경마다 다르게 나옵니다.

12문자열 파서

문자열을 해석하는 코드는 없고, 배열의 첫 원소를 꺼내는 제네릭 함수만 있습니다. 파서를 만들 때도 토큰 목록의 맨 앞을 들여다보는 이런 함수가 기본 재료가 됩니다. 여기서 T를 any로 바꾸면 반환값의 타입 정보가 사라져 뒤따르는 자동완성과 검사까지 무력해집니다.

TypeScript code

// 문자열 파서
function first<T>(values: T[]): T | undefined {
  return values[0];
}
console.log(first(["a", "b"]));
알아두면 좋은 점

제네릭은 입력과 출력 타입의 관계를 묶어 주므로, 문자열 배열을 넘기면 결과가 string | undefined로 자동으로 좁혀집니다.

자주 하는 실수

T를 any로 바꾸면 반환값에 어떤 속성을 붙여도 통과해, 오타 난 메서드 호출이 실행 시점까지 그대로 살아남습니다.

13함수 팩토리

팩토리 대신 두 수를 더해 값을 반환하는 평범한 함수가 정의돼 있습니다. 설정을 미리 받아 함수를 만들어 내는 구조로 확장하려면 이 함수를 감싸는 바깥 함수가 필요합니다. 함수 선언문은 호이스팅되어 정의보다 위에서도 호출되지만, const에 담은 함수 표현식은 그렇지 않습니다.

TypeScript code

// 함수 팩토리
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(13, 2));
알아두면 좋은 점

function 선언은 스코프 맨 위로 끌어올려져 정의보다 먼저 호출할 수 있지만, const에 담은 함수는 선언 줄 이전에 접근하면 오류가 납니다.

자주 하는 실수

팩토리 안에서 바깥의 let 변수를 참조하면, 나중에 그 변수가 바뀔 때 이미 만들어 둔 함수의 동작까지 함께 달라집니다.

14메모이제이션

캐시 없이 매번 배열 전체를 순회해 합을 다시 계산합니다. 결과를 재사용하려면 인자를 키로 삼는 저장소를 함수 바깥에 두어야 합니다. 나머지 매개변수는 호출마다 새 배열을 만들기 때문에, 배열 자체를 키로 쓰면 값이 같아도 참조가 달라 캐시가 절대 맞지 않습니다.

TypeScript code

// 메모이제이션
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 14));
알아두면 좋은 점

인자가 하나뿐이라면 그 값을 Map의 키로 쓰면 되고, 객체를 키로 삼되 수명을 함께 두고 싶다면 WeakMap이 적합합니다.

자주 하는 실수

나머지 매개변수는 호출마다 새 배열을 만들므로, 배열 자체를 캐시 키로 쓰면 참조가 매번 달라져 캐시가 한 번도 맞지 않습니다.

15커링

커링 형태는 아니지만, 함수가 함수를 반환한다는 같은 뼈대를 makeCounter가 보여 줍니다. 인자를 하나씩 나눠 받는 커링도 결국 이렇게 안쪽 함수를 돌려주는 구조입니다. 반환 타입을 적지 않았어도 TypeScript가 () => number로 추론하므로 next()는 숫자로 다뤄집니다.

TypeScript code

// 커링
function makeCounter() {
  let current: number = 0;
  return () => ++current;
}
const next = makeCounter();
console.log(next(), next());
알아두면 좋은 점

반환 타입을 적지 않아도 () => number로 추론되지만, 공개 API라면 명시해 두어야 구현이 바뀔 때 시그니처 변화가 눈에 띕니다.

자주 하는 실수

커링 중간 단계를 변수에 담아 재사용하면 안쪽 클로저가 상태를 공유해, 한 호출에서 쌓인 값이 다른 호출로 새어 나갑니다.

16compose 함수

합성 함수는 없고, 배열의 첫 값을 꺼내는 제네릭 함수 하나가 전부입니다. 여러 함수를 이어 붙이는 compose를 만들 때도 각 조각은 이처럼 작은 단일 책임 함수여야 합니다. 제네릭 함수를 합성에 넘기면 타입 매개변수가 그 자리에서 고정되므로, 호출마다 다른 타입으로 재사용되지는 않습니다.

TypeScript code

// compose 함수
function first<T>(values: T[]): T | undefined {
  return values[0];
}
console.log(first(["a", "b"]));
알아두면 좋은 점

여러 함수를 잇는 compose는 오버로드나 가변 제네릭으로 단계별 타입을 이어 줘야 결과 타입이 끝까지 유지됩니다.

자주 하는 실수

제네릭 함수를 값으로 넘기면 그 자리에서 타입 매개변수가 하나로 고정되므로, 같은 조합을 여러 타입에 돌려 쓰려던 계획이 어긋납니다.

17제네릭 함수

제네릭이 아니라 number로 고정된 덧셈 함수이며, add(17, 2)의 결과를 출력합니다. 다루는 타입이 하나뿐이라면 굳이 타입 매개변수를 도입할 이유가 없습니다. 이 함수를 <T>로 일반화하면 + 연산이 임의의 T에는 허용되지 않아 제약을 따로 걸어야 합니다.

TypeScript code

// 제네릭 함수
function add(a: number, b: number): number {
  return a + b;
}
console.log(add(17, 2));
알아두면 좋은 점

다루는 타입이 하나뿐인 함수에 제네릭을 붙이면 호출부의 오류 메시지만 길어지고, 실제로 좁혀 주는 정보는 늘지 않습니다.

자주 하는 실수

<T>(a: T, b: T) 형태로 일반화하면 임의의 T에는 +가 정의되지 않아, 제약을 걸기 전까지 컴파일되지 않습니다.

18콜백으로 완료 알림

완료 콜백은 등장하지 않고, 인자로 받은 숫자들을 즉시 합산해 동기적으로 반환합니다. 결과를 기다릴 필요가 없는 계산은 콜백이나 Promise로 감싸지 않는 편이 단순합니다. 안쪽 reduce 콜백은 값을 돌려주기만 할 뿐 완료를 알리는 콜백과는 역할이 전혀 다릅니다.

TypeScript code

// 콜백으로 완료 알림
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}
console.log(sum(1, 2, 18));
알아두면 좋은 점

동기 계산은 프로미스로 감싸지 않는 편이 낫습니다. 호출부가 await을 붙이는 순간 실행이 마이크로태스크 뒤로 밀리기 때문입니다.

자주 하는 실수

동기 함수를 콜백 스타일로 바꾸면서 콜백을 즉시 호출하면, 어떤 경로는 동기로 어떤 경로는 비동기로 끝나 실행 순서가 예측 불가능해집니다.

19reducer 함수

리듀서와 비슷하게 이전 값을 바탕으로 다음 값을 만들지만, 상태를 인자로 받지 않고 클로저 안에 감춰 둡니다. 액션 로그가 필요 없는 지역 카운터라면 이 방식이 훨씬 가볍습니다. 반대로 상태가 함수 밖에서 보이지 않기 때문에 스냅숏을 찍거나 되돌리는 동작은 만들 수 없습니다.

TypeScript code

// reducer 함수
function makeCounter() {
  let current: number = 0;
  return () => ++current;
}
const next = makeCounter();
console.log(next(), next());
알아두면 좋은 점

상태를 인자로 받는 리듀서는 같은 입력에 같은 출력을 내므로, 액션을 다시 재생해 특정 시점을 그대로 복원할 수 있습니다.

자주 하는 실수

클로저에 감춘 상태는 밖에서 읽을 수 없어, 스냅숏 저장이나 되돌리기가 필요해지는 순간 구조를 통째로 바꿔야 합니다.

20파이프라인 함수

파이프라인 대신 배열의 첫 원소를 반환하는 함수 하나가 정의되고, 문자열 배열에 대해 호출됩니다. 여러 단계를 잇는 흐름에서도 각 단계는 이렇게 입력 하나에 출력 하나인 모양이 좋습니다. values[0]은 인덱스 검사를 켜지 않는 한 타입상 T로 취급되므로, 반환 타입의 undefined는 개발자가 직접 적어 준 것입니다.

TypeScript code

// 파이프라인 함수
function first<T>(values: T[]): T | undefined {
  return values[0];
}
console.log(first(["a", "b"]));
알아두면 좋은 점

각 단계를 입력 하나에 출력 하나로 맞춰 두면 순서를 바꾸거나 단계를 빼도 나머지 코드에 영향이 가지 않습니다.

자주 하는 실수

중간 단계가 undefined를 돌려줄 수 있는데 다음 단계가 이를 고려하지 않으면, 속성 접근에서 실행 중에 예외가 납니다.

정리하며

  • 타입을 붙이면 추론이 콜백 인자까지 흐르지만 any는 그 흐름을 끊습니다
  • 화살표 함수는 정의된 자리의 this를 쓰고 일반 함수는 호출 방식을 따릅니다
  • 메서드를 콜백으로 넘길 때는 화살표 형태나 명시적 바인딩이 필요합니다
  • 제네릭 함수는 입력 타입과 반환 타입의 관계를 그대로 표현합니다

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