항상 const
또는 let
을 사용하여 변수를 선언하자. 변수를 재할당해야 하는 경우가 아니면 기본적으로 const
를 사용하고 var
는 사용하지 말자. (const
와 let
은 대부분 다른 언어의 변수처럼 블록 스코프이지만, var
는 함수 스코프로 이해하기 어려운 버그를 일으킬 수 있다.)
변수는 선언 전에 사용되어서는 안 된다.
한 번에 하나의 변수 선언만 하자. (let a = 1, b = 2;
금지)
Array()
생성자를 사용을 피하자. 혼란스럽고 모순되는 사용법이 존재하기 때문이다.
Before
const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];
After
const a = [2];
const b = [2, 3];
// new Array(2) 대신
const c = [];
c.length = 2;
// [0, 0, 0, 0, 0]
Array.from<number>({length: 5}).fill(0);
배열에 숫자가 아닌 속성을 정의하거나 사용하지 말자(length
는 제외).
배열은 본래 순차적인 데이터 저장을 위한 자료구조이기 때문에, 배열의 인덱스(즉, 숫자형 인덱스) 외에 임의의 문자열 속성을 정의하는 것은 혼란을 초래할 수 있다.
대신 Map
(또는 Object
)을 사용하자.
Before
const arr = [1, 2, 3];
arr.customProperty = "Hello";
console.log(arr.customProperty); // "Hello"
console.log(arr.length); // 3 (길이는 영향을 받지 않음)
for (const key in arr) {
console.log(key); // "0", "1", "2", "customProperty"(예상치 못한 속성)
}
console.log(Object.keys(arr)); // ["0", "1", "2", "customProperty"]
After
const map = new Map();
map.set("customProperty", "Hello");
map.set(0, 1);
map.set(1, 2);
console.log(map.get("customProperty")); // "Hello"
console.log(map.get(0)); // 1
스프레드 문법은 이터러블(iterable) 객체를 펼쳐서 새로운 배열이나 객체를 만들 때 유용하지만, 그 사용에 있어서 몇 가지 주의해야 할 점이 있다.
스프레드 문법은 이터러블만 펼쳐야 한다:
스프레드 문법을 사용할 때 펼칠 값은 이터러블이어야 한다. 이터러블이란 배열, 문자열, 맵, 세트 같은 데이터 구조를 말하며, 그 안의 요소들을 순차적으로 접근할 수 있다.
원시 값(primitive types)인 null
, undefined
, 숫자, 문자열 등의 값은 스프레드 문법을 사용할 수 없다.
Before
const foo = [7];
const bar = [5, ...(shouldUseFoo && foo)]; // 에러 발생할 수 있음
After
const foo = shouldUseFoo ? [7] : []; // 조건에 따라 배열 선택.
const bar = [5, ...foo];
스프레드되는 값은 생성되는 값과 자료형과 구조를 일치하는 것이 권장된다:
Before
const fooStrings = ['a', 'b', 'c'];
const ids = {...fooStrings}; // {0: 'a', 1: 'b', 2: 'c'}
After
const fooStrings = ['a', 'b', 'c'];
const ids = [...fooStrings, 'd', 'e']; // ['a', 'b', 'c', 'd', 'e']
스프레드 문법은 얕은 복사(shallow copy)를 수행한다.
얕은 복사는 객체나 배열의 1단계 깊이까지만 복사한다. 따라서 다음과 같은 상황이 발생한다:
const arr = [1, 2, 3]; // 기본 자료형만 포함된 배열
const spreadArr = [...arr];
spreadArr[0] = 99;
console.log(arr); // [1, 2, 3] (원본 불변)
console.log(spreadArr); // [99, 2, 3] (복사본만 변경됨)
const arr = [1, 2, { a: 3 }]; // 참조 자료형이 포함된 배열
const spreadArr = [...arr];
spreadArr[2].a = 10;
console.log(arr); // [1, 2, { a: 10 }] (원본도 영향받음)
console.log(spreadArr); // [1, 2, { a: 10 }] (복사본도 동일)
구조 분해 할당은 코드의 가독성을 높이고, 변수를 더 쉽게 추출할 수 있도록 돕는 유용한 기능이다. 하지만 올바르게 사용하는 방법이 중요하다.
배열을 구조 분해하여 변수에 값을 할당하는 방법은 다음과 같다:
const [a, b, c, ...rest] = generateResults();
let [, b,, d] = someArray;
배열에 기본값을 설정하는 방법:
Before
function destructuring([a, b] = [4, 2]) { … }
After
function destructured([a = 4, b = 2] = []) { … }
배열 대신 객체 구조 분해를 사용하는 것이 더 좋은 경우가 많다. 배열 구조 분해는 순서대로 값을 추출하므로, 배열의 순서가 바뀌면 코드가 깨질 수 있다. 반면, 객체 구조 분해는 키 이름으로 값을 추출하기 때문에 순서에 의존하지 않아 더 안전하고 직관적이다.
const { name, age, location } = user;
Object()
생성자는 객체를 생성하는 데 사용될 수 있지만, 객체 리터럴 {}을 사용하는 것이 훨씬 더 간결하고 명확하다.
Before
let obj = new Object();
obj.a = 1;
obj.b = 2;
After
let obj = { a: 1, b: 2 };
for (... in ...)
는 객체의 모든 열거 가능한(enumerable) 속성을 순회한다.
여기에는 해당 객체의 직접 속성뿐만 아니라 프로토타입 체인에서 상속된 속성도 포함될 수 있으므로 주의하자.
Before
const someObj = { a: 1, b: 2 };
Object.prototype.c = 3; // 프로토타입에 추가된 속성
for (const key in someObj) {
console.log(key); // 출력: "a", "b", "c" (예상치 못한 속성 포함)
}
After
for (const key in someObj) {
if (someObj.hasOwnProperty(key)) {
console.log(key); // 출력: "a", "b" (프로토타입 속성 제외)
}
}
hasOwnProperty
는 해당 속성이 객체의 직접 속성인지 확인for (const key of Object.keys(someObj)) {
console.log(key); // 출력: "a", "b"
}
Object.keys()
는 객체의 직접 열거 가능한 속성의 키만 반환for (const [key, value] of Object.entries(someObj)) {
console.log(key, value); // 출력: "a 1", "b 2"
}
Object.entries()
는 [키, 값] 쌍의 배열을 반환스프레드 문법은 기존 객체의 얕은 복사(shallow copy)를 생성한다.
중복 키가 있는 경우, 나중에 정의된 값이 기존 값을 덮어쓴다.
const foo = { num: 1 };
const foo2 = {
...foo,
num: 5,
}; // 기존 foo의 내용을 복사하고, num을 5로 덮어씀.
const foo3 = {
num: 5,
...foo,
}; // foo의 내용을 복사하면서 기존 num: 5를 덮어씀.
console.log(foo2.num); // 출력: 5
console.log(foo3.num); // 출력: 1
객체를 생성할 때 오직 객체만 스프레드하자:
배열과 기본형(null과 undefined 포함)을 스프레드 해서는 안된다.
const fooStrings = ['a', 'b', 'c'];
const ids = {...fooStrings}; // {0: 'a', 1: 'b', 2: 'c'}
기본 Object
프로토타입이 아닌 다른 프로토타입을 갖는 객체(예: 클래스 정의, 클래스 인스턴스, 함수)의 스프레드는 주의해야 한다:
스프레드 문법은 객체의 열거 가능한(enumerable) 비-프로토타입(non-prototype) 속성만 복사한다. (Object.keys(obj)
로 반환되는 속성들)
커스텀 프로토타입을 가지는 객체는 프로토타입 체인에 있는 속성(메서드 등)은 복사되지 않는다.
계산된 프로퍼티명(computed property names)를 사용하면 더 동적으로 키를 정의할 수 있다:
JavaScript에서 객체 리터럴에서의 속성 정의는 보통 정적인 문자열이나 식별자를 사용하지만, Computed property names는 객체의 키를 런타임 시 계산된 값으로 설정할 때 사용된다.
const dynamicKey = 'age';
const obj = {
[dynamicKey]: 25, // 'age': 25
};
console.log(obj.age); // 25
계산된 속성이 심볼(예: [Symbol.iterator])이 아닌 한, dict 스타일(따옴표로 묶인) 키로 간주된다. (즉, 따옴표로 묶이지 않은 키와 혼합하지 말자).
구조 분해는 단순한 한 단계 수준에서만 사용이 권장된다:
중첩된 구조 분해는 가독성을 해치고, 매개변수의 복잡성을 증가시킨다. 중첩된 구조를 분해해야 한다면, 함수를 나누어 처리하거나 별도로 객체를 추출하여 사용하는 것이 낫다.
Before
function nestedTooDeeply({x: {num, str}}: {x: Options}) {}
After
interface Options {
num?: number;
str?: string;
}
function destructured({num, str = 'default'}: Options = {}) {}
기본값은 구조 분해의 왼쪽에서 명시적으로 설정한다.
Before
function destructured({num, str}: Options = {num: 42, str: 'default'}) {
console.log(num, str);
}
After
function destructured({num = 42, str = 'default'}: Options = {}) {
console.log(num, str);
}
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