함수 생성에는 세 가지 주요 방식이 있다.
Function Declaration (함수 선언):
function
키워드를 사용하여 함수 선언문으로 작성된 함수.function myFunction() {
console.log("This is a function declaration");
}
Function Expression (함수 표현식):
const myFunction = function () {
console.log("This is a function expression");
};
setTimeout(function () {
console.log("This is also a function expression");
}, 1000);
Arrow Function (화살표 함수):
=>
문법을 사용하여 작성된 함수.this
와 arguments
는 상위 스코프를 상속받음(일반 함수와의 주요 차이점).const add = (a, b) => a + b; // Concise body
const multiply = (a, b) => {
return a * b; // Block body
};
return
키워드를 사용하여 값을 명시적으로 반환해야 함.명명된 함수 정의시 함수 선언문이 기본적으로 선호된다.
hoistedFunction(); // 정상 작동
function hoistedFunction() {
console.log("I'm hoisted!");
}
화살표 함수와 함수 표현식을 남용하면 디버깅 어려움과 코드의 불명확성을 초래할 수 있으므로 적절한 상황에서 사용해야 한다.
const foo = () => {
throw new Error("Something went wrong");
};
foo(); // 에러 스택에 "foo"가 아닌 "anonymous function"으로 표시될 수 있음.
화살표 함수는 특별한 요구사항이 있을 때(예: 타입 주석 필요, 콜백 간결화) 적합하다.
interface SearchFunction {
(source: string, subString: string): boolean;
}
const fooSearch: SearchFunction = (source, subString) => {
return source.includes(subString);
};
const numbers = [1, 2, 3];
const squared = numbers.map(n => n * n); // 화살표 함수 활용
this
바인딩이 필요 없는 경우:class Example {
values = [1, 2, 3];
multiply() {
return this.values.map(v => v * 2); // this는 상위 스코프의 것을 사용
}
}
this
를 상위 스코프에서 상속받기 때문에 객체 메서드와 같이 this
바인딩이 필요하지 않은 곳에 적합함수가 다른 메서드(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
선언문을 사용할 경우, this
가 undefined
이거나 다른 컨텍스트로 바인딩될 수 있다.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();
함수 표현식(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; }
함수의 목적에 따라 간결한 표현식(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 사용
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);
setTextFn
은 HTMLElement
를 매개변수로 받고, this
를 사용하지 않는다.bind
메서드를 사용해 document.body
를 명시적으로 전달했다.this
를 신뢰하지 말고, 화살표 함수 또는 명시적 매개변수 전달을 사용하자.
화살표 함수는 this
바인딩 문제를 우아하게 해결하며, 명시적 매개변수 전달은 더욱 명확하고 안전한 코드를 작성할 수 있다.
콜백은 예상치 못한 인수로 호출될 수 있으며, 이는 유형 검사를 통과할 수 있지만 여전히 논리적 오류를 발생시킬 수 있다. 이를 해결하기 위해 명시적인 매개변수 전달 방식을 권장한다.
선택적 매개변수에 주의:
Before
const numbers = ['11', '5', '10'].map(parseInt);
// 결과: [11, NaN, 2]
map
은 parseInt
를 호출할 때, 세 가지 매개변수(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
를 호출한다.클래스 속성으로 화살표 함수를 정의하는 것을 가능하면 피하자.
클래스의 속성을 화살표 함수로 초기화하는 경우, 함수 호출 시 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
를 바인딩.이벤트 핸들러의 설치와 제거를 효과적으로 관리하는 방법
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
는 호출 시마다 새로운 함수 참조를 반환한다.addEventListener
와 removeEventListener
는 동일한 참조를 사용해야 핸들러를 제거할 수 있다.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!');
}
}
선택적 매개변수는 함수 호출 시 값을 전달하지 않더라도 기본값을 사용할 수 있게 하여 코드의 유연성을 높인다. 하지만 잘못 사용하면 코드의 예상치 못한 동작이나 부작용을 초래할 수 있다.
간단한 기본값 사용:
선택적 매개변수에는 간단하고 부작용이 없는 기본값을 설정해야 한다.
기본값은 단순한 값이나 불변 데이터(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();
}
}
지역 변수나 매개변수의 이름으로 arguments
사용금지 (내장된 이름임).
가변 인수 함수를 작성할 때 더 명확하고 안전한 코드를 작성하기 위해 rest parameters
와 spread 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
는 타입 추론을 더 잘 지원한다.함수 몸체의 시작 또는 끝에 빈 줄 금지:
함수의 시작과 끝에 빈 줄을 두는 것은 불필요한 공백을 추가하여 코드가 산만해 보이게 만든다.
코드의 흐름을 간결하고 명확하게 유지하기 위해 빈 줄은 중간 논리적 그룹화를 제외하고 피해야 한다.
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 };
}
even
과 odd
계산은 서로 다른 논리적 작업이므로 빈 줄로 구분했다.*
위치 지정:
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);
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: 명시적으로 함수 호출
(비어있음)
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