Google의 TypeScript Style Guide > Language features > 4.5 Functions

FeelsBotMan·2024년 12월 6일
0

GTS

목록 보기
6/8
post-thumbnail

4 Language features

4.5 Functions

4.5.1 Terminology

함수 생성에는 세 가지 주요 방식이 있다.

Function Declaration (함수 선언):

  • function 키워드를 사용하여 함수 선언문으로 작성된 함수.
  • 이름이 필수적이며, 코드 블록의 최상위나 함수 내부에서 정의될 수 있다.
  • 호이스팅(hoisting)으로 함수 정의 전에 호출 가능.
function myFunction() {
    console.log("This is a function declaration");
}

Function Expression (함수 표현식):

  • function 키워드를 사용하여 표현식의 일부로 작성된 함수.
  • 변수에 할당되거나 다른 함수의 매개변수로 전달됨.
  • 이름이 선택적이며, 주로 익명 함수(anonymous function)로 사용됨.
  • 호이스팅되지 않으며, 정의되기 전에 호출할 수 없음.
const myFunction = function () {
    console.log("This is a function expression");
};

setTimeout(function () {
    console.log("This is also a function expression");
}, 1000);

Arrow Function (화살표 함수):

  • => 문법을 사용하여 작성된 함수.
  • 익명 함수로만 사용 가능하며, 간결하고 직관적인 문법 제공.
  • thisarguments는 상위 스코프를 상속받음(일반 함수와의 주요 차이점).
  • 간단한 연산에 적합하지만, 메서드로 사용되거나 복잡한 작업에는 부적합할 수 있음.
  • 두 가지 형태(Concise/Block Body)를 가짐.
const add = (a, b) => a + b; // Concise body
const multiply = (a, b) => {
    return a * b; // Block body
};
  • Concise Body (간결한 본문): 결과 값은 암묵적으로 반환된다.
  • Block Body (블록 본문): return 키워드를 사용하여 값을 명시적으로 반환해야 함.

4.5.2 Prefer function declarations for named functions

명명된 함수 정의시 함수 선언문이 기본적으로 선호된다.

  • 함수 선언문은 이름이 필수적이므로 디버깅 시 함수 이름이 스택 추적(trace)에 표시된다.
  • 함수 선언문은 스코프 내 어디서든 호출 가능하다. 이는 코드 구조를 더 유연하게 만들어 준다.
  • 코드가 직관적이고 읽기 쉽다. 특히 팀 프로젝트에서 명확한 함수 이름은 큰 장점이다.
hoistedFunction(); // 정상 작동
function hoistedFunction() {
    console.log("I'm hoisted!");
}

화살표 함수와 함수 표현식을 남용하면 디버깅 어려움과 코드의 불명확성을 초래할 수 있으므로 적절한 상황에서 사용해야 한다.

const foo = () => {
    throw new Error("Something went wrong");
};
foo(); // 에러 스택에 "foo"가 아닌 "anonymous function"으로 표시될 수 있음.

화살표 함수는 특별한 요구사항이 있을 때(예: 타입 주석 필요, 콜백 간결화) 적합하다.

  1. 명시적인 타입 주석이 필요한 경우:
interface SearchFunction {
    (source: string, subString: string): boolean;
}

const fooSearch: SearchFunction = (source, subString) => {
    return source.includes(subString);
};
  1. 간결한 콜백 함수:
const numbers = [1, 2, 3];
const squared = numbers.map(n => n * n); // 화살표 함수 활용
  1. this 바인딩이 필요 없는 경우:
class Example {
    values = [1, 2, 3];
    multiply() {
        return this.values.map(v => v * 2); // this는 상위 스코프의 것을 사용
    }
}
  • 화살표 함수는 this를 상위 스코프에서 상속받기 때문에 객체 메서드와 같이 this 바인딩이 필요하지 않은 곳에 적합

4.5.3 Nested functions

함수가 다른 메서드(method)나 함수 내부에 중첩되어 정의될 때, 상황에 따라 function 선언문(function declarations) 또는 화살표 함수(arrow functions)를 사용할 수 있다.

메서드 내부에서 화살표 함수 선호:
화살표 함수는 this를 상위 스코프에서 상속받는다. 메서드 내부에서 중첩된 함수가 화살표 함수로 정의되면, 메서드의 this를 유지한다.

Before

class Example {
  value = 10;

  method() {
    function nested() {
      console.log(this.value); // undefined
    }

    nested(); // this는 글로벌 컨텍스트 또는 undefined
  }
}
  • function 선언문을 사용할 경우, thisundefined이거나 다른 컨텍스트로 바인딩될 수 있다.

After

class Example {
  value = 10;

  method() {
    // 화살표 함수 사용
    const nested = () => {
      console.log(this.value); // 상위 스코프의 this를 사용
    };

    nested();
  }
}

const instance = new Example();
instance.method(); // 10

독립적인 this가 필요할 때 function 선언문 사용:

function outerFunction() {
  this.value = 5;

  function nested() {
    console.log(this.value); // 독립된 this
  }

  nested.call({ value: 42 }); // 42
}

outerFunction();

4.5.4 Do not use function expressions

함수 표현식(function expressions)을 사용하지 말고, 대신 화살표 함수(arrow functions)를 사용해야 하는 경우.

  • 간결한 문법: 화살표 함수는 코드의 가독성과 간결성을 높인다.
  • this의 일관성: 화살표 함수는 상위 스코프의 this를 상속받아, 호출 컨텍스트에 따라 this가 달라지는 문제를 방지한다.

Before

bar(function() {
  this.doSomething(); // 호출 컨텍스트에 따라 this가 달라질 수 있음
});

After

bar(() => {
  this.doSomething(); // 상위 스코프의 this 사용
});

특정 상황에서는 호출 컨텍스트에 따라 this를 재바인딩해야 할 때 함수 표현식을 사용할 수 있다. 하지만 이는 권장되지 않는다.

bar(function() {
  // 여기서의 this는 호출 컨텍스트에 따라 다름
  this.doSomething();
}.bind(newContext)); // 명시적으로 this를 바인딩

Generator 함수는 화살표 함수로 정의할 수 없기 때문에 함수 표현식을 사용해야 한다.

function* generatorFunction() {
  yield 1;
  yield 2;
}

4.5.5 Arrow function bodies

함수의 목적에 따라 간결한 표현식(concise body)을 사용할지, 블록 표현식(block body)을 사용할지를 판단하자.

  • 간결한 표현식: 반환값이 사용되는 경우에만 사용. 또는, void를 사용해 반환값이 없음을 명시적으로 표현할 수도 있다.
  • 블록 표현식: 반환값이 없거나 명령문이 필요한 경우에 사용.

Before

myPromise.then(v => console.log(v)); // BAD: 반환값이 사용되지 않는데 간결한 표현식을 사용한 경우

After

myPromise.then(v => {
  console.log(v); // GOOD: 블록 표현식으로 반환값이 없음을 명시
});

Before

let f: () => void;
f = () => 1; // BAD: f는 void를 반환해야 하지만, 1이 암묵적으로 반환되므로 오류 발생 가능.

After

myPromise.then(v => void console.log(v)); // GOOD: 반환값을 무시하는 void 사용

4.5.6 Rebinding this

JavaScript에서는 this의 값이 함수 호출 방법에 따라 달라지므로 실수하기 쉽다. 이를 방지하기 위해 화살표 함수명시적인 매개변수 전달을 권장하고 있다.

bind 메서드를 통해 this를 재바인딩하는 것은 과거에는 자주 사용되던 방식이지만, 화살표 함수가 등장한 이후로는 덜 추천되는 방법이다.

document.body.onclick = clickHandler.bind(this); // `this`를 명시적으로 바인딩

bind는 불필요한 코드가 늘어나고, 실수로 잘못된 컨텍스트를 바인딩할 위험이 있다. 대신 화살표 함수나 매개변수 전달 방식으로 더 간결하게 작성할 수 있다.

화살표 함수는 상위 스코프의 this를 유지한다. 따라서, this가 의도치 않게 변경되는 문제를 방지한다.
Before

function clickHandler() {
  this.textContent = 'Hello';
}

document.body.onclick = clickHandler; // BAD: `this`가 불명확

After

document.body.onclick = () => {  // GOOD: 화살표 함수로 상위 this 유지
  document.body.textContent = 'hello';  
};
  • 화살표 함수는 this를 상위 컨텍스트에서 가져온다.
  • 여기서는 this를 직접 사용하지 않고, document.body를 명시적으로 참조하므로 더 안전하다.

명시적으로 객체를 함수의 매개변수로 전달하면 this 문제를 완전히 피할 수 있다.

const setTextFn = (e: HTMLElement) => {  // GOOD: 명시적 매개변수 전달
  e.textContent = 'hello';
};

document.body.onclick = setTextFn.bind(null, document.body);
  • setTextFnHTMLElement를 매개변수로 받고, this를 사용하지 않는다.
  • bind 메서드를 사용해 document.body를 명시적으로 전달했다.
  • 이 방식은 객체 참조를 코드에서 더 명확히 드러낸다.

this를 신뢰하지 말고, 화살표 함수 또는 명시적 매개변수 전달을 사용하자.
화살표 함수는 this 바인딩 문제를 우아하게 해결하며, 명시적 매개변수 전달은 더욱 명확하고 안전한 코드를 작성할 수 있다.


4.5.7 Prefer passing arrow functions as callbacks

콜백은 예상치 못한 인수로 호출될 수 있으며, 이는 유형 검사를 통과할 수 있지만 여전히 논리적 오류를 발생시킬 수 있다. 이를 해결하기 위해 명시적인 매개변수 전달 방식을 권장한다.

선택적 매개변수에 주의:
Before

const numbers = ['11', '5', '10'].map(parseInt);
// 결과: [11, NaN, 2]
  • mapparseInt를 호출할 때, 세 가지 매개변수(element, index, array)를 전달한다.
  • parseInt에서 두 번째 매개변수는 radix(진수)로 사용된다.
  • 결과적으로:
    '11'parseInt('11', 0)11 (기본적으로 10진수)
    '5'parseInt('5', 1)NaN (1은 유효한 진수가 아님)
    '10'parseInt('10', 2)2 (2진수 해석 결과)

After

const numbers = ['11', '5', '3'].map((n) => parseInt(n));
// 결과: [11, 5, 3]
  • 화살표 함수로 전달 매개변수(n)를 명확히 지정하여 의도한 대로 parseInt를 호출한다.

4.5.8 Arrow functions as properties

클래스 속성으로 화살표 함수를 정의하는 것을 가능하면 피하자.

클래스의 속성을 화살표 함수로 초기화하는 경우, 함수 호출 시 this가 이미 고정되어 있어, 호출자가 this를 명시적으로 이해해야 한다. 이는 코드의 가독성과 예측 가능성을 저하시킬 수 있다.
Before

class DelayHandler {
  constructor() {
    // Bad: `this.patienceTracker`가 화살표 함수 속성으로 초기화됨.
    setTimeout(this.patienceTracker, 5000);
  }
  private patienceTracker = () => {
    this.waitedPatiently = true;
  };
}
  • patienceTracker가 이미 화살표 함수로 정의되어, this가 항상 DelayHandler의 인스턴스를 가리킨다.
  • 호출자 입장에서 this.patienceTracker는 일반적인 인스턴스 메서드처럼 보이지만, 사실은 이미 바인딩된 함수로 작동한다.
  • 호출자에게 추가적인 "비지역적인 이해"가 요구된다. 즉, patienceTracker가 바인딩된 함수라는 사실을 호출 위치에서 알기 어렵다.

After
대안 1: 익명 함수로 호출

class DelayHandler {
  constructor() {
    // 익명 함수로 `this`를 명시적으로 관리.
    setTimeout(() => {
      this.patienceTracker();
    }, 5000);
  }
  private patienceTracker() {
    this.waitedPatiently = true;
  }
}
  • 익명 함수 내부에서 this.patienceTracker()를 호출하므로, 호출자가 this가 바인딩되는 방식을 명확히 알 수 있다.
  • patienceTracker는 일반적인 클래스 메서드처럼 보이며, 호출자가 추가적인 이해 없이 사용할 수 있다.

대안 2: bind 사용

class DelayHandler {
  constructor() {
    // `this.patienceTracker`를 호출 시점에 바인딩.
    setTimeout(this.patienceTracker.bind(this), 5000);
  }
  private patienceTracker() {
    this.waitedPatiently = true;
  }
}
  • 장점:
    • bind를 통해 호출 시점에 this를 바인딩.
    • 익명 함수를 정의하지 않고도 동작.
  • 단점:
    • 코드가 조금 더 복잡해 보일 수 있음.
    • 메서드 호출의 "한 번 더 감싸기"가 필요.

4.5.9 Event handlers

이벤트 핸들러의 설치와 제거를 효과적으로 관리하는 방법

Before
이벤트 핸들러를 설치할 때 bind를 사용하면 임시 참조가 생성되며, 이는 핸들러 제거 시 문제를 일으킨다.

class Component {
  onAttached() {
    // 이 `bind`는 새로운 참조를 생성.
    window.addEventListener('onbeforeunload', this.listener.bind(this));
  }
  onDetached() {
    // 이 `bind`는 위의 참조와 다르므로 제거에 실패.
    window.removeEventListener('onbeforeunload', this.listener.bind(this));
  }
  private listener() {
    confirm('Do you want to exit the page?');
  }
}
  • 새로운 참조 생성:
    - bind는 호출 시마다 새로운 함수 참조를 반환한다.
    - addEventListenerremoveEventListener는 동일한 참조를 사용해야 핸들러를 제거할 수 있다.
  • 메모리 누수 위험:
    - 이벤트가 제거되지 않으면, 핸들러 내부의 this가 계속 참조되어 클래스 인스턴스가 메모리에서 해제되지 않는다.

After
핸들러가 설치 제거를 요구하는 경우, 화살표 함수 속성이 올바른 접근 방식이다. 이 속성은 자동으로 설치 제거를 캡처하고 설치 제거에 대한 안정적인 참조를 제공하기 때문이다.

class Component {
  private listener = () => {
    confirm('Do you want to exit the page?');
  };

  onAttached() {
    // 안정적인 참조를 사용해 이벤트 핸들러 설치.
    window.addEventListener('onbeforeunload', this.listener);
  }

  onDetached() {
    // 동일한 참조로 핸들러 제거 가능.
    window.removeEventListener('onbeforeunload', this.listener);
  }
}
  • 안정적인 참조:
    - 화살표 함수는 this를 바인딩하고, 참조가 변하지 않는다.
  • 메모리 누수 방지:
    - removeEventListener에서 동일한 참조를 사용하므로 핸들러 제거가 확실히 이루어진다.

이벤트를 클래스 내부에서만 발행하고 핸들러를 제거할 필요가 없을 때는, 익명 화살표 함수도 사용할 수 있다.

class Component {
  onAttached() {
    // 익명 화살표 함수 사용. 핸들러 제거가 필요하지 않은 경우.
    this.addEventListener('click', () => {
      this.listener();
    });
  }

  private listener() {
    console.log('Clicked!');
  }
}
  • 이벤트가 클래스 내부에서만 발행되고, 제거가 필요 없을 때 간결한 코드 작성 가능.
  • 핸들러를 제거해야 하는 경우 이 접근법은 적합하지 않다.

4.5.10 Parameter initializers

선택적 매개변수는 함수 호출 시 값을 전달하지 않더라도 기본값을 사용할 수 있게 하여 코드의 유연성을 높인다. 하지만 잘못 사용하면 코드의 예상치 못한 동작이나 부작용을 초래할 수 있다.

간단한 기본값 사용:
선택적 매개변수에는 간단하고 부작용이 없는 기본값을 설정해야 한다.
기본값은 단순한 값이나 불변 데이터(immutable data)를 사용하여 부작용을 방지한다.
Before

let globalCounter = 0;
function newId(index = globalCounter++) {}
  • globalCounter++는 호출할 때마다 globalCounter의 값을 변경.
  • 호출 순서나 횟수에 따라 결과가 달라질 수 있다.
  • 전역 상태를 사용하므로 의도치 않은 전역 상태 공유 문제가 발생.

After

function newId(index?: number) {
  if (index === undefined) {
    index = globalCounter++;
  }
}
  • 기본값이 복잡하거나 동적인 경우, 기본값 설정 논리를 함수 내부로 이동하여 부작용을 방지한다.

디스트럭처링 사용:
선택적 매개변수가 많거나 순서를 지정하기 어렵다면 디스트럭처링을 사용해 가독성을 높인다.

function configure({ size = 10, color = 'red', enabled = true } = {}) {
  console.log(size, color, enabled);
}

기본값으로 공유 상태를 노출하지 않는다:
공유 상태를 기본값으로 사용하면 함수 호출 간에 상태가 공유되어 의도치 않은 커플링(coupling)이 발생한다.
Before

class Foo {
  private readonly defaultPaths: string[];
  frobnicate(paths = defaultPaths) {}
}
  • defaultPaths는 클래스 수준의 공유 상태.
  • 함수의 호출 결과가 외부 상태에 따라 달라지므로 디버깅이 어렵다.

After

frobnicate(paths?: string[]) {
  if (!paths) {
    paths = this.defaultPaths.slice();
  }
}

4.5.11 Prefer rest and spread when appropriate

지역 변수나 매개변수의 이름으로 arguments 사용금지 (내장된 이름임).

가변 인수 함수를 작성할 때 더 명확하고 안전한 코드를 작성하기 위해 rest parametersspread syntax를 사용할 것을 권장한다. 이는 arguments 객체의 단점을 피하고 최신 문법을 활용하려는 것이다.

arguments 대신 rest parameters 사용:
arguments는 함수 호출 시 전달된 모든 인수를 포함한 유사 배열 객체다.
arguments는 전달된 모든 인수를 담지만, 함수의 매개변수 이름이 아니기 때문에 가독성이 떨어진다.
arguments는 타입스크립트에서 타입이 제대로 추론되지 않기 때문에 타입 안전성을 보장하지 않는다.
Before

function sum() {
  let total = 0;
  for (const num of arguments) {
    total += num; // 어떤 인수를 처리하는지 명확하지 않음
  }
  return total;
}

After

function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}
  • ...numbers는 함수가 숫자들의 리스트를 받는다는 것을 명확히 나타낸다.

Function.prototype.apply 대신 spread syntax 사용:
apply는 배열을 인수로 받아 호출하는데, 최신 문법에서는 spread syntax를 사용하여 더 간결하고 명확하게 작성할 수 있다.
Before

function callWithApply(func: Function, args: any[]) {
  func.apply(null, args); // 인수를 배열로 전달
}

After

function callWithSpread(func: (...args: any[]) => void, args: any[]) {
  func(...args); // 스프레드 문법 사용
}
  • 인수를 배열로 펼치는 의도가 더 명확함
  • 타입스크립트에서 spread syntax는 타입 추론을 더 잘 지원한다.

4.5.12 Formatting functions

함수 몸체의 시작 또는 끝에 빈 줄 금지:
함수의 시작과 끝에 빈 줄을 두는 것은 불필요한 공백을 추가하여 코드가 산만해 보이게 만든다.
코드의 흐름을 간결하고 명확하게 유지하기 위해 빈 줄은 중간 논리적 그룹화를 제외하고 피해야 한다.

Before

function myFunction() {

  const a = 1;
  const b = 2;

  return a + b;

}

After

function myFunction() {
  const a = 1;
  const b = 2;

  return a + b;
}

함수 내부에서 빈 줄을 사용해 논리적 그룹화:
함수 내부에서 관련 없는 코드 블록을 논리적으로 구분하기 위해 빈 줄을 적절히 사용할 수 있다.
지나치게 빈 줄을 많이 사용하면 오히려 코드가 읽기 어려워지므로 필요할 때만 사용한다.

function processArray(array: number[]) {
  const even = array.filter(x => x % 2 === 0);

  const odd = array.filter(x => x % 2 !== 0);

  return { even, odd };
}
  • evenodd 계산은 서로 다른 논리적 작업이므로 빈 줄로 구분했다.

* 위치 지정:
Generator 함수 선언 시, *는 함수명과 붙이는 것이 가독성에 더 좋다.
yield* 역시 별표 *와 키워드 yield를 붙여서 표현한다.
이 규칙은 함수의 구조를 명확히 보여주고, 별표와 키워드가 하나의 연산을 나타냄을 강조한다.
Before

function *generatorFunction() {
  yield *[1, 2, 3];
}

After

function* generatorFunction() {
  yield* [1, 2, 3];
}

단일 인수 화살표 함수의 괄호 사용:
단일 인수 화살표 함수에서는 괄호가 선택 사항이지만, 권장된다.
괄호를 사용하면 코드를 더 명확하고 일관되게 보이게 한다.
괄호를 생략해도 동작하지만, 스타일 가이드에서는 항상 괄호를 사용하도록 권장한다.
Before

const square = x => x * x;

After

const square = (x) => x * x;

... 뒤의 공백 없음:
Rest나 Spread 문법을 사용할 때, ... 뒤에 공백을 넣지 않는 것이 표준이다.
공백이 없을 때 가독성이 더 좋으며, 문법적으로도 의도된 연산을 명확히 드러낸다.
Before

function myFunction(... elements: number[]) {}
myFunction(... array, ... iterable);

After

function myFunction(...elements: number[]) {}
myFunction(...array, ...iterable);

4.6 this

this는 자바스크립트에서 문맥에 따라 값이 달라지기 때문에, 잘못 사용하면 버그를 초래하거나 이해하기 어려운 코드를 만들 수 있다.

this는 클래스 내부 또는 명시적으로 타입이 정의된 곳에서만 사용하고 전역 문맥이나 call/apply, 이벤트 핸들러에서의 사용은 화살표 함수나 명시적인 변수 참조로 대체하자.

this를 사용하지 말아야 하는 경우:

전역 객체(global object)를 참조:
전역 객체를 참조하려고 this를 사용하는 것은 명확하지 않고 위험하다.
전역 문맥에서 this는 브라우저에서는 window, Node.js에서는 global을 참조할 수 있는데, 이는 코드의 문맥에 따라 달라지기 때문에 예측하기 어렵다.

// BAD: 전역 문맥에서 this를 사용
this.alert('Hello'); // 브라우저에서는 window.alert를 호출

eval 문맥에서 this 사용:
eval은 코드 보안 및 디버깅 문제를 초래할 수 있으며, this와 함께 사용하면 의도치 않은 동작을 유발할 수 있다.

eval('this.alert("Hello")'); // BAD: 불명확한 동작

이벤트 핸들러에서의 this 참조:
전통적인 이벤트 핸들러에서의 this는 이벤트를 발생시킨 DOM 요소를 참조한다. 이는 클래스 인스턴스와 혼동될 가능성이 높다.
Before

document.getElementById('button').onclick = function() {
  console.log(this); // this는 버튼 요소를 참조
};

After

document.getElementById('button').onclick = () => {  // GOOD: 화살표 함수 사용
  console.log(this); // this는 외부 문맥(예: 클래스)을 참조
};

불필요한 call 또는 apply 사용:
this를 필요 없이 call()이나 apply()로 전달하는 것은 코드의 명확성을 떨어뜨리고 불필요한 복잡성을 추가한다.
Before

function greet() {
  console.log(this.message);
}

greet.call({ message: 'Hello' });  // BAD: call을 사용하여 this 전달

After

const context = { message: 'Hello' };
function greet() {
  console.log(context.message);
}
greet();  // GOOD: 명시적으로 함수 호출

4.7 Interfaces

(비어있음)


참고자료

Google TypeScript Style Guide

GitHub - google/gts: ☂️ TypeScript style guide, formatter, and linter.

Typescript Google Code Style Part 1
Typescript Google Code Style Part 2
Typescript Google Code Style Part 3

ts.dev - TypeScript style guide

profile
안드로이드 페페

0개의 댓글