Skip to main content

좋은 TypeScript 코드는 멋진 트릭에서 시작하지 않습니다. 의외로 출발점은 아주 소박합니다. 함수를 잘 만드는 것입니다.

함수는 프로그램의 작은 기계입니다. 무언가를 넣으면, 규칙에 따라 처리하고, 결과를 내놓습니다. TypeScript는 이 작은 기계에 설명서를 붙여 줍니다. 무엇을 넣어야 하는지, 무엇이 나오는지, 어떤 값은 생략해도 되는지, 심지어 다른 함수를 전달받는지도 미리 알려 줍니다.

자바스크립트에서는 함수가 꽤 자유롭습니다. 자유는 멋지지만, 가끔은 너무 자유로워서 개발자를 놀라게 합니다. TypeScript는 그 자유를 완전히 없애지 않으면서, 적어도 함수 앞에는 안내문을 세워 둡니다. 그래서 실수로 엉뚱한 값을 넣거나, 반환값을 오해하는 일을 줄여 줍니다.

함수 시그니처는 계약서다

함수를 볼 때 가장 먼저 봐야 하는 것은 함수 이름이 아니라 함수 시그니처입니다. 시그니처는 이 함수가 어떤 입력을 받고 어떤 출력을 내는지 보여 주는 계약서입니다.1

가장 기본적인 예시는 이렇습니다.

function add(a: number, b: number): number {
  return a + b;
}

이 함수는 이렇게 읽으면 됩니다.

a와 b라는 두 개의 숫자를 받아서, 숫자를 하나 돌려주는 함수.

아주 단순하지만, 여기에는 중요한 약속이 다 들어 있습니다.

  • 매개변수 a는 number

  • 매개변수 b는 number

  • 반환값은 number

여기서 잠깐, 초보자가 자주 헷갈리는 단어를 정리하고 가겠습니다.

매개변수(parameter)는 함수 정의에 적는 변수이고, 인자(argument)는 함수를 호출할 때 실제로 넘기는 값입니다.

function greet(name: string) {
  return `안녕하세요, ${name}님!`;
}

greet('지민');

위 코드에서 name은 매개변수이고, '지민'은 인자입니다.

TypeScript에서는 함수 타입 자체를 따로 적을 수도 있습니다. 예를 들어 이렇게요.1

type GreetFunction = (name: string) => string;

이 코드는 선언문처럼 보이진 않지만, 뜻은 아주 분명합니다.

name이라는 문자열을 받아서 문자열을 돌려주는 함수 타입.

화살표 기준으로 생각하면 쉽습니다.

  • 왼쪽: 들어오는 것

  • 오른쪽: 나가는 것

함수 시그니처를 읽는 습관이 생기면, 코드를 보기 전에 이미 함수의 성격을 절반쯤 이해하게 됩니다.

매개변수에 타입 붙이기

TypeScript에서 함수 매개변수에 타입을 붙이는 건 어렵지 않습니다. 변수에 타입을 붙이는 것과 거의 같은 느낌입니다.

function introduce(name: string, age: number): string {
  return `${name}님은 ${age}살입니다.`;
}

이제 이 함수는 문자열과 숫자를 정확히 받아야 합니다.

introduce('민수', 20);   // 정상
introduce('민수', '20'); // 오류

두 번째 호출이 막히는 이유는 단순합니다. 함수가 숫자를 원한다고 계약서에 적혀 있는데 문자열을 들고 왔기 때문입니다.

화살표 함수도 똑같이 타입을 붙일 수 있습니다.

const multiply = (a: number, b: number): number => {
  return a * b;
};

또는 함수 변수 자체에 함수 타입을 먼저 적을 수도 있습니다.

const divide: (a: number, b: number) => number = (a, b) => {
  return a / b;
};

이 방식이 좋은 이유는 함수의 모양을 먼저 못 박을 수 있기 때문입니다. 마치 '이 자리에 들어올 직원은 이런 역할을 해야 합니다'라고 채용 공고를 먼저 써 두는 것과 비슷합니다.

초보자에게는 첫 번째 방식이 더 읽기 쉽고, 규모가 커지면 두 번째 방식이 더 재사용하기 좋습니다.

반환값은 함수의 결말이다

함수의 매개변수가 입력이라면, 반환값은 결말입니다. 영화를 보는데 결말이 없으면 허무하듯, 함수도 무엇을 돌려주는지 분명해야 이해하기 쉽습니다.

function square(num: number): number {
  return num * num;
}

: number는 이 함수가 반드시 숫자를 반환해야 한다는 뜻입니다.2

TypeScript는 때로 반환 타입을 스스로 추론할 수 있습니다.

function square(num: number) {
  return num * num;
}

이 코드도 문제는 없습니다. TypeScript는 num * num이 숫자라는 걸 알기 때문에 반환 타입을 number로 이해합니다.2

그런데 초보자에게는 이렇게 생각하는 습관이 꽤 유용합니다.

짧고 명백한 함수는 추론에 맡겨도 되지만, 중요한 함수는 반환 타입을 직접 적자.

왜냐하면 반환 타입을 적는 순간, 함수가 해야 할 역할이 더 또렷해지기 때문입니다. 나중에 실수로 문자열을 반환하게 바뀌어도 바로 잡아낼 수 있습니다.

반환값이 없는 함수도 있습니다.

function logMessage(message: string): void {
  console.log(message);
}

void는 값을 돌려주지 않는다는 뜻입니다.2

이런 함수는 결과를 만드는 대신, 화면에 출력하거나 파일에 저장하거나 상태를 바꾸는 식으로 행동을 합니다. 즉, 계산기보다는 스위치에 가깝습니다.

선택적 인자: 있어도 되고 없어도 되는 값

모든 함수 인자가 항상 필요한 것은 아닙니다. 어떤 함수는 기본 정보만 있어도 동작하고, 추가 정보는 있으면 더 잘 동작합니다. 이럴 때 쓰는 것이 선택적 매개변수입니다.

function greet(name: string, title?: string): string {
  if (title !== undefined) {
    return `${title} ${name}님, 안녕하세요!`;
  }
  return `${name}님, 안녕하세요!`;
}

title?: string은 문자열이 올 수도 있고, 아예 안 올 수도 있다는 뜻입니다.3

호출은 이렇게 됩니다.

greet('다은');
greet('다은', '개발자');

중요한 규칙이 하나 있습니다. 선택적 매개변수는 필수 매개변수 뒤에 와야 합니다.3

// 잘못된 예시
function badExample(title?: string, name: string) {
  return `${title} ${name}`;
}

왜 안 될까요? 호출하는 쪽에서 인자 하나만 넣었을 때, 그 값이 title인지 name인지 헷갈리기 때문입니다.

선택적 매개변수를 사용할 때 또 하나 조심해야 할 것이 있습니다. 바로 if (value) 같은 검사입니다. 이 방식은 편해 보이지만, 0, '', false 같은 값도 없다고 착각할 수 있습니다.3

function retry(taskName: string, delayMs?: number) {
  if (delayMs) {
    console.log(`${delayMs}ms 뒤에 ${taskName} 재시도`);
  } else {
    console.log(`${taskName} 즉시 재시도`);
  }
}

retry('업로드', 0);

이 코드는 0을 전달했는데도 마치 값을 안 준 것처럼 처리할 수 있습니다. 그래서 선택적 값은 가능하면 이렇게 검사하는 편이 안전합니다.

function retry(taskName: string, delayMs?: number) {
  if (delayMs !== undefined) {
    console.log(`${delayMs}ms 뒤에 ${taskName} 재시도`);
  } else {
    console.log(`${taskName} 즉시 재시도`);
  }
}

이제 0은 진짜 값으로 인정받습니다. 숫자 0도 소중한 시민입니다.

기본값이 있는 매개변수는 더 친절하다

선택적 매개변수와 비슷하지만 느낌이 다른 것이 기본값 매개변수입니다.

function power(value: number, exponent: number = 2): number {
  return value ** exponent;
}

이 함수는 exponent를 생략하면 자동으로 2를 사용합니다.2

power(3);    // 9
power(3, 3); // 27

선택적 매개변수와 기본값 매개변수의 차이는 생각보다 큽니다.

선택적 매개변수는 함수 안에서 undefined일 수 있습니다. 그래서 확인이 필요합니다. 반면 기본값 매개변수는 값이 빠져도 자동으로 채워지기 때문에 함수 안에서는 더 다루기 편합니다.3

function greet(name: string, message: string = '안녕하세요'): string {
  return `${message}, ${name}님!`;
}

이 함수 안에서 message는 언제나 문자열입니다. 그래서 별도 검사 없이 바로 사용할 수 있습니다.

실무 감각으로 정리하면 이렇습니다.

  • 빠졌을 때 쓸 만한 기본값이 있다면 기본값 매개변수

  • 빠졌는지 자체가 중요한 정보라면 선택적 매개변수

  • 선택 가능한 값이 너무 많아지면 함수 설계를 다시 생각하기

매개변수 세 개, 네 개, 다섯 개가 모두 선택적이라면 함수가 '유연한 것'이 아니라 '헷갈리는 것'일 수도 있습니다.

콜백: 함수를 인자로 받는 함수

이제 함수의 진짜 매력을 볼 차례입니다. 자바스크립트와 TypeScript에서 함수는 값처럼 다룰 수 있습니다. 즉, 숫자나 문자열처럼 다른 함수에 전달할 수 있습니다.1

이때 전달되는 함수를 콜백(callback)이라고 부릅니다. 쉽게 말해, '일이 끝나면 이 함수를 불러 주세요'라는 예약 버튼입니다.

function calculate(a: number, b: number, callback: (result: number) => void): void {
  const result = a + b;
  callback(result);
}

이 시그니처는 이렇게 읽습니다.

숫자 두 개를 받고, 숫자 결과를 전달받는 함수를 하나 더 받아서, 최종적으로는 아무 값도 반환하지 않는 함수.

사용은 이렇게 합니다.

calculate(3, 4, (result) => {
  console.log(`계산 결과는 ${result}입니다.`);
});

여기서 세 번째 인자는 함수입니다. callback: (result: number) => void라는 타입 덕분에, 콜백이 숫자를 받아야 한다는 사실을 TypeScript가 알고 있습니다. 그래서 콜백 안의 result도 자연스럽게 숫자로 이해합니다.

콜백 타입이 길어지면 이름을 붙여 주는 편이 읽기 좋습니다.

type ResultHandler = (result: number) => void;

function calculate(a: number, b: number, handler: ResultHandler): void {
  handler(a + b);
}

이제 함수 시그니처가 훨씬 덜 복잡해 보입니다. 긴 설명을 별명으로 바꿔 둔 셈입니다.

재미있는 예로, 라면 완성 알림 함수를 만들어 볼 수 있습니다.

type Done = (message: string) => void;

function cookRamen(waterMl: number, done: Done): void {
  const message = `물 ${waterMl}ml로 라면이 완성되었습니다!`;
  done(message);
}

cookRamen(550, (message) => {
  console.log(message);
});

물론 실제 라면은 함수보다 냄비가 더 중요하지만, 콜백을 이해하는 데는 꽤 훌륭한 예시입니다.

콜백의 선택적 매개변수는 신중하게

콜백에도 선택적 매개변수를 붙일 수 있습니다.

type FetchCallback = (data: string, success?: boolean) => void;

문법상 가능하고, 실제로도 쓸 수 있습니다.4

하지만 여기에는 함정이 있습니다. 콜백 매개변수를 선택적으로 표시하면, 그 콜백을 받는 쪽은 그 값이 없을 수도 있다고 항상 생각해야 합니다. 즉, 편해 보이지만 오히려 처리 코드가 복잡해질 수 있습니다.

const handleFetch: FetchCallback = (data, success) => {
  if (success === true) {
    console.log(`성공: ${data}`);
  } else {
    console.log('실패 또는 상태 정보 없음');
  }
};

그래서 설계할 때는 이렇게 판단하면 좋습니다.

  • 정말 어떤 호출에서는 그 값을 생략할 계획인가?

  • 생략되었을 때의 의미가 분명한가?

  • 차라리 기본값이나 다른 구조가 더 읽기 쉽지 않은가?

콜백의 선택적 매개변수는 '유연함'도 주지만 '애매함'도 함께 데려옵니다. 초보자일수록 남발하지 않는 편이 좋습니다.

함수 시그니처 읽기 연습

이제 함수 시그니처를 실제로 읽어 봅시다.

function savePost(title: string, content: string, published?: boolean): void

이 함수는 이렇게 읽습니다.

문자열 제목과 문자열 본문을 받고, 공개 여부를 뜻하는 불리언 값은 선택적으로 받으며, 반환값은 없다.

다음은 콜백이 들어간 경우입니다.

function fetchUserName(id: number, onSuccess: (name: string) => void): void

이 함수는 이렇게 읽습니다.

숫자 아이디를 받고, 사용자 이름 문자열을 전달받는 콜백 함수를 하나 받으며, 함수 자체는 아무 값도 반환하지 않는다.

조금 더 복잡한 것도 읽어 볼 수 있습니다.

type Formatter = (value: string, upper?: boolean) => string;

이 타입은 이렇게 읽으면 됩니다.

문자열 하나를 받고, 대문자 변환 여부는 선택적으로 받으며, 최종적으로 문자열을 반환하는 함수 타입.

함수 시그니처를 읽을 때는 순서를 정해 두면 쉽습니다.

  • 무엇을 받는가?

  • 무엇이 선택 사항인가?

  • 무엇을 돌려주는가?

  • 함수 말고 다른 함수를 받는가?

이 네 가지만 보이면 대부분의 함수는 겁먹지 않고 읽을 수 있습니다.

좋은 함수 설계는 좋은 질문에서 나온다

TypeScript 문법을 아는 것과 함수를 잘 설계하는 것은 조금 다릅니다. 문법은 도구이고, 설계는 판단입니다.

함수를 만들기 전에 스스로에게 이런 질문을 해 보세요.

  • 이 함수가 꼭 받아야 하는 값은 무엇인가?

  • 빠져도 되는 값은 무엇인가?

  • 결과를 반환하는 편이 좋은가, 아니면 콜백으로 넘기는 편이 좋은가?

  • 반환 타입을 보면 이 함수의 목적이 바로 드러나는가?

예를 들어, 이런 함수는 조금 아쉽습니다.

function doStuff(a: string, b?: number, c?: boolean): any {
  // ...
}

이 코드는 동작할 수는 있어도, 읽는 사람 입장에서는 안개가 낀 것처럼 흐릿합니다. 이름도 모호하고, 반환값도 any이고, 선택적 값도 많습니다.

반면 이런 함수는 의도가 더 잘 드러납니다.

function createNickname(name: string, maxLength: number = 8): string {
  return name.slice(0, maxLength);
}

이 함수는 이름만 봐도 목적이 보이고, 매개변수도 이해되고, 반환값도 분명합니다.

좋은 TypeScript 코드는 대단한 고급 타입보다 먼저, 이런 선명한 함수에서 출발합니다.

작은 정리: 오늘 꼭 챙겨 갈 것

이 장의 핵심은 문법 암기가 아닙니다. 함수의 모양을 읽고, 설명할 수 있는 감각입니다.

  • 함수 시그니처는 입력과 출력을 보여 주는 계약서다.

  • 매개변수 타입은 함수가 무엇을 받아야 하는지 명확하게 만든다.

  • 반환 타입은 함수가 무엇을 약속하는지 보여 준다.

  • 선택적 매개변수와 기본값 매개변수는 비슷해 보여도 역할이 다르다.

  • 콜백은 함수를 값처럼 전달하는 방식이며, 타입을 붙이면 훨씬 안전해진다.

함수를 잘 만든다는 말은 결국 이런 뜻입니다. 이 함수가 어떤 일을 하고, 어떻게 써야 하는지 한눈에 보이게 만드는 것.

TypeScript는 그 일을 도와주는 아주 성실한 안내원입니다. 다만 안내판을 세우는 일은 여전히 개발자의 몫입니다.

참고

1TypeScript Handbook - More on Functions

2TypeScript Functions

3TypeScript Optional Parameters

4TypeScript Optional Parameters in Callbacks

책 목록

한 번에 이해되는 TypeScript: 초보자를 위한 가장 쉬운 타입스크립트 입문

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기