99명

최근 30분간 동시 방문자 수를 표시합니다. (~)

최고 동시 방문자 수 -
어제: 0명 / 오늘: 0명

필수 JS + TS 핵심 문법 정리

필수 JS + TS 핵심 문법 정리

JavaScript와 TypeScript 문법 중에서 웹 개발을 할 때 반복적으로 사용하는 것들을 정리했습니다.
React, Vue, Svelte 같은 프레임워크의 코드도 별도의 새로운 문법이 아니라 이 문법 위에서 작성됩니다.
그래서 언어 자체가 익숙하지 않으면, 프레임워크를 학습하는 과정에서 프레임워크가 아닌 문법 때문에 막힐 수 있습니다.
JavaScript를 처음 배우는 단계이거나, 문법을 대강 알지만 정리가 되지 않은 분을 대상으로 합니다.

각 문법은 정의, 바로 실행해 볼 수 있는 예제, 자주 하는 실수 순서로 설명합니다.
코드의 실행 결과는 주석으로 함께 표시했습니다.
JavaScript를 먼저 다루고, 그 위에 타입을 더하는 TypeScript를 이어서 다룹니다.

각 주제를 더 자세히 살펴보려면 JS 함수 핵심 패턴, JS 비동기 핵심 패턴, JS 클래스 핵심 패턴, 한눈에 보는 타입스크립트 글을 참고하세요.

# JavaScript

브라우저와 Node.js 어디에서나 그대로 실행되는 JavaScript 문법입니다.
대부분의 예제는 브라우저 개발자 도구의 콘솔(Console)에 붙여 넣어서 실행해 볼 수 있습니다.

# 변수와 스코프

변수는 const와 let으로 선언합니다.

  • const는 재할당(다른 값을 다시 대입하는 것)이 불가능하고, let은 재할당이 가능합니다.
  • 기본적으로 const를 사용하고, 값을 다시 대입해야 할 때만 let을 사용합니다.
  • var는 오래된 선언 방식으로, 아래에서 설명하는 스코프 문제 때문에 사용하지 않습니다.
JS
1
2
3
4
5
const name = 'Kim' let count = 0 count = 1 // let은 재할당 가능 // name = 'Lee' // 에러, const는 재할당 불가

변수가 유효한 범위를 스코프(Scope)라고 합니다.
const와 let은 중괄호({ })로 감싼 블록 단위의 스코프를 가지므로, 블록 밖에서는 접근할 수 없습니다.

const와 let은 블록 안에 머물고, var는 블록을 넘어 함수 전체에서 보입니다

JS
1
2
3
4
5
6
7
8
let message = '바깥' if (true) { let message = '안쪽' // 블록 안에서만 유효한 별개의 변수 console.log(message) // '안쪽' } console.log(message) // '바깥'

var는 함수 단위 스코프라 블록을 무시합니다.
반복문의 변수가 반복문 밖으로 새어 나오는 등 예상하기 어려운 동작이 생기므로, 새로 작성하는 코드에서는 const와 let만 사용합니다.

JS
1
2
3
4
5
for (var i = 0; i < 3; i++) {} console.log(i) // 3, 블록 밖에서도 접근됨 for (let j = 0; j < 3; j++) {} // console.log(j) // 에러, j는 반복문 블록 안에서만 유효

# 값과 참조

값을 저장하고 다루는 방식은 두 가지로 나뉩니다.

  • 원시 타입(Primitive Type): number, string, boolean, null, undefined 등. 변수에 값 자체를 저장합니다.
  • 참조 타입(Reference Type): 객체, 배열, 함수. 변수에는 값이 있는 위치(참조, Reference)를 저장합니다.

원시 타입은 값을 복사하고, 참조 타입은 같은 객체를 가리킵니다

이 차이 때문에 비교와 복사의 결과가 달라집니다.
원시 타입은 값이 같으면 같다고 판단하지만, 객체는 내용이 같아도 위치가 다르면 다른 객체입니다.

JS
1
2
3
4
5
6
1 === 1 // true 'abc' === 'abc' // true, 원시 타입은 값으로 비교 const obj1 = { a: 1 } const obj2 = { a: 1 } obj1 === obj2 // false, 내용이 같아도 다른 객체

객체를 다른 변수에 대입하면 객체가 복사되는 것이 아니라 참조가 복사됩니다.
두 변수는 같은 객체를 가리키므로, 한쪽에서 바꾸면 다른 쪽에서도 바뀐 값이 보입니다.

JS
1
2
3
4
5
const a = [1, 2] const b = a // 같은 배열을 가리킴, 복사가 아님 b.push(3) console.log(a) // [1, 2, 3], a도 함께 바뀜

함수에 객체를 전달할 때도 마찬가지입니다.
함수 안에서 매개변수로 받은 객체를 바꾸면 함수 밖의 원본이 바뀝니다.

JS
1
2
3
4
5
6
7
function addItem(cart, item) { cart.push(item) // 전달받은 배열을 직접 변경 } const cart = ['사과'] addItem(cart, '바나나') console.log(cart) // ['사과', '바나나'], 원본이 바뀜

원본을 바꾸지 않고 데이터를 다루는 방법은 불변성과 복사에서 다룹니다.

# 템플릿 리터럴

템플릿 리터럴(Template Literal)은 백틱(Backtick, 키보드에서 숫자 1 왼쪽에 있는 기호)으로 문자열을 감싸는 문법입니다.
${ } 안에 값이나 표현식을 작성하면 그 결과가 문자열에 포함되며, 이것을 보간(Interpolation)이라고 합니다.

JS
1
2
3
4
5
6
7
8
const name = 'Kim' const age = 20 const id = 7 const page = 2 const greeting = `안녕하세요, ${name}님` // 변수 보간 const info = `${name}님은 ${age}세, 내년에는 ${age + 1}세입니다` // 표현식도 보간 가능 const url = `/api/users/${id}?page=${page}` // '/api/users/7?page=2', 주소 조합에 자주 사용

줄바꿈도 그대로 문자열에 포함되므로, 여러 줄 문자열을 작성할 때도 사용합니다.

JS
1
2
3
4
5
6
const isActive = true const html = ` <li class="${isActive ? 'active' : ''}"> ${name} </li> ` // isActive가 true면 class="active"

isActive ? 'active' : ''는 조건에 따라 두 값 중 하나를 고르는 삼항 연산자로, 다음 절에서 설명합니다.

# 조건과 단축 평가

비교는 항상 ===와 !==를 사용합니다.
==는 타입을 자동으로 변환한 뒤에 비교하므로 예상 밖의 결과를 만듭니다.

JS
1
2
3
4
'1' == 1 // true, 자동 변환되어 혼란을 줌 '1' === 1 // false, 타입까지 비교 null == undefined // true null === undefined // false

조건문은 값을 참 같은 값(Truthy)과 거짓 같은 값(Falsy)으로 판단합니다.
Falsy로 취급되는 값은 아래 몇 가지로 정해져 있고, 나머지는 모두 Truthy입니다.

구분 값
Falsy false, 0, '', null, undefined, NaN. 그리고 -0, 0n
Truthy 그 밖의 모든 값. '0', 'false', [], {}도 Truthy
JS
1
2
3
if (list.length) {} // 길이가 0이면 실행되지 않음 if (user) {} // user가 null이나 undefined면 실행되지 않음 if ([]) {} // 빈 배열도 Truthy라 실행됨

삼항 연산자(Ternary Operator)는 조건에 따라 두 값 중 하나를 고르는 표현식입니다.
if문과 달리 값을 반환하므로 변수 대입이나 템플릿 안에서 바로 사용합니다.

JS
1
const label = count > 0 ? `${count}개` : '없음'

&&와 ||는 논리 연산자이지만, 결과가 정해지는 순간 평가를 멈추고 그 시점의 값을 반환합니다.
이것을 단축 평가(Short-circuit Evaluation)라고 하며, 값을 고르거나 조건부로 실행하는 용도로 자주 사용합니다.

  • A || B: A가 Truthy면 A, 아니면 B. 기본값을 지정할 때 사용합니다.
  • A && B: A가 Falsy면 A, 아니면 B. A가 참일 때만 B를 실행할 때 사용합니다.
JS
1
2
const title = user.nickname || '익명' // nickname이 없으면 '익명' isOpen && close() // isOpen이 참일 때만 close 호출

||는 0이나 ''처럼 유효한 값도 Falsy라서 건너뛰는 문제가 있습니다.
null 병합 연산자(Nullish Coalescing Operator) ??는 왼쪽 값이 null이나 undefined일 때만 오른쪽 값을 사용합니다.

JS
1
2
const page = options.page || 1 // page가 0이면 1로 바뀜 const page2 = options.page ?? 1 // page가 0이면 0 유지, null이나 undefined일 때만 1

??=는 값이 null이나 undefined일 때만 대입하는 연산자입니다.
||=와 &&=도 같은 방식으로 동작합니다.

JS
1
config.timeout ??= 3000 // timeout이 없을 때만 3000 대입

옵셔널 체이닝(Optional Chaining) ?.은 중간 값이 null이나 undefined이면 에러 없이 undefined를 반환합니다.
중첩된 객체에 접근할 때, 중간 값이 없어서 프로그램이 멈추는 것을 방지합니다.

JS
1
2
3
4
5
6
7
const user = { profile: { name: 'Kim' } } user.profile.name // 'Kim' // user.account.name // 에러, account가 undefined라 그 다음 접근에서 멈춤 user.account?.name // undefined, 에러 없이 넘어감 user.profile?.address?.city // 단계마다 안전하게 접근

함수 호출이나 배열 접근에도 사용할 수 있습니다.

JS
1
2
user.getName?.() // getName이 있을 때만 호출 list?.[0] // list가 있을 때만 첫 요소 접근

?.와 ??를 함께 사용하면 값이 없을 때의 기본값까지 한 줄로 처리할 수 있습니다.

JS
1
const city = user.profile?.address?.city ?? '주소 없음'

# 함수

함수는 선언식, 표현식, 화살표 함수 세 가지 방식으로 만듭니다.
동작은 같지만 화살표 함수(Arrow Function)가 짧아서 콜백에 특히 자주 사용합니다.

JS
1
2
3
4
5
6
7
8
9
function add(a, b) { // 함수 선언식 return a + b } const subtract = function (a, b) { // 함수 표현식 return a - b } const multiply = (a, b) => a * b // 화살표 함수

화살표 함수의 형태와 축약 규칙입니다.

  • 형태는 (매개변수) => 반환값입니다.
  • 본문이 표현식 하나면 중괄호와 return을 생략할 수 있습니다(암묵적 반환).
  • 매개변수가 하나면 소괄호를 생략할 수 있습니다.
  • 객체를 암묵적으로 반환할 때는 중괄호가 본문으로 해석되지 않도록 소괄호로 감쌉니다.
JS
1
2
3
4
5
6
const double = n => n * 2 // 매개변수 1개는 괄호 생략 가능 const makeUser = name => ({ name }) // 객체 반환은 소괄호로 감쌈 const area = (w, h) => { // 본문이 여러 줄이면 중괄호와 return 사용 const result = w * h return result }

매개변수에는 기본값(Default Value)을 지정할 수 있습니다.
인자를 전달하지 않거나 undefined를 전달하면 기본값이 사용됩니다.

JS
1
2
3
4
const greet = (name = '손님') => `안녕하세요, ${name}` greet('Kim') // '안녕하세요, Kim' greet() // '안녕하세요, 손님'

옵션이 여러 개인 함수는 매개변수를 나열하는 대신 객체 하나로 받습니다.
호출할 때 순서를 기억할 필요가 없고, 필요한 옵션만 전달할 수 있습니다.
객체를 받아 바로 분해하는 문법은 구조 분해 할당에서 다룹니다.

JS
1
2
3
4
5
6
function request({ url, method = 'GET', retry = 0 }) { // 객체를 받아 바로 분해 console.log(method, url, retry) } request({ url: '/api/users' }) // 'GET', '/api/users', 0 request({ url: '/api/users', method: 'POST' }) // 'POST', '/api/users', 0

화살표 함수는 자신만의 this가 없고, 선언된 위치의 바깥 this를 그대로 사용합니다.
객체의 메소드나 클래스 안에서 this를 사용해야 한다면 일반 함수나 메소드 축약 문법을 사용합니다.
this의 동작 방식은 JS 함수 핵심 패턴에서 자세히 다룹니다.

# 콜백과 고차 함수

콜백(Callback)은 다른 함수에 인자로 전달하는 함수입니다.
전달받은 함수는 자신이 정한 시점에 콜백을 호출합니다.
함수를 인자로 받거나 함수를 반환하는 함수를 고차 함수(Higher-order Function)라고 하며, 배열의 map과 filter, 이벤트 리스너 등록이 대표적입니다.

JS
1
2
3
4
5
6
7
8
9
10
11
12
const button = document.querySelector('button') button.addEventListener('click', () => { console.log('클릭됨') // 클릭될 때마다 실행 }) setTimeout(() => { console.log('1초 뒤 실행') }, 1000) const nums = [1, 2, 3] nums.forEach(n => console.log(n)) // 요소마다 실행

함수를 전달할 때는 호출 결과가 아니라 함수 자체를 전달합니다.
괄호를 붙이면 그 자리에서 즉시 실행되고, 반환값(대개 undefined)이 전달됩니다.

JS
1
2
3
4
5
6
function handleClick() { console.log('클릭됨') } button.addEventListener('click', handleClick) // 함수 자체를 전달 button.addEventListener('click', handleClick()) // 즉시 실행한 결과(undefined)를 전달, 실수

콜백에 인자를 미리 정해서 전달하고 싶다면 화살표 함수로 한 번 감쌉니다.

JS
1
button.addEventListener('click', () => handleClick('저장')) // 클릭 시점에 handleClick('저장') 실행

# 구조 분해 할당

구조 분해 할당(Destructuring Assignment)은 객체나 배열에서 필요한 값을 꺼내 변수에 할당하는 문법입니다.
객체는 속성 이름으로, 배열은 순서로 값을 꺼냅니다.

구조 분해 할당은 객체에서는 이름으로, 배열에서는 순서로 값을 꺼냅니다

JS
1
2
3
4
5
const user = { id: 1, name: 'Kim', age: 20 } const { name, age } = user // 필요한 값만 추출 const { name: userName } = user // userName이라는 다른 이름으로 const { role = 'guest' } = user // 값이 없으면 기본값 'guest'

중첩된 객체도 단계별로 분해할 수 있습니다.
다만 깊이가 깊어지면 읽기 어려우므로 두 단계 정도까지만 사용합니다.

JS
1
2
3
const data = { user: { name: 'Kim', address: { city: '서울' } } } const { user: { address: { city } } } = data // city는 '서울'

함수의 매개변수 자리에서 바로 분해하는 패턴을 매우 자주 사용합니다.
프레임워크의 컴포넌트가 props를 받는 방식도 이 문법입니다.

JS
1
2
3
4
5
function printUser({ name, age }) { console.log(`${name} (${age})`) } printUser(user) // 'Kim (20)'

배열은 인덱스(Index) 순서대로 값을 꺼냅니다.
필요 없는 자리는 쉼표로 건너뜁니다.

JS
1
2
const [first, second] = [10, 20] // first는 10, second는 20 const [, , third] = [1, 2, 3] // 앞 두 개는 건너뛰고 third는 3

여러 값을 반환하는 함수는 배열이나 객체로 묶어서 반환하고, 받는 쪽에서 분해합니다.
반환 순서가 정해져 있으면 배열을, 이름으로 구분하고 싶으면 객체를 사용합니다.

JS
1
2
3
4
5
6
7
8
9
10
function getSize(el) { return [el.offsetWidth, el.offsetHeight] // 순서로 구분 } const [width, height] = getSize(box) function parseNumber(text) { const value = Number(text) return { value, ok: !Number.isNaN(value) } // 이름으로 구분 } const { value, ok } = parseNumber('42') // value는 42, ok는 true

# 전개와 나머지

...는 자리에 따라 두 가지로 동작합니다.
값을 펼치는 전개(Spread)와, 남은 값을 하나로 모으는 나머지(Rest)입니다.

전개는 하나를 여럿으로 펼치고, 나머지는 여럿을 하나로 모읍니다

전개는 객체나 배열을 펼쳐서 복사하거나 합칩니다.

JS
1
2
3
4
5
6
7
8
const a = [1, 2] const b = [3, 4] const merged = [...a, ...b] // [1, 2, 3, 4] const copy = [...a] // 원본과 별개인 복사본 const base = { name: 'Kim', age: 20 } const updated = { ...base, age: 21 } // age만 덮어쓴 새 객체

배열을 함수의 인자로 펼쳐서 전달할 때도 사용합니다.

JS
1
2
const nums = [5, 2, 8] Math.max(...nums) // Math.max(5, 2, 8)과 동일, 8

객체를 합칠 때 속성 이름이 겹치면 뒤에 오는 값이 앞의 값을 덮어씁니다.
기본 설정에 사용자 설정을 덮어쓰는 패턴에 사용합니다.

JS
1
2
3
const defaults = { theme: 'light', lang: 'ko' } const custom = { lang: 'en' } const settings = { ...defaults, ...custom } // { theme: 'light', lang: 'en' }

나머지는 남은 여러 값을 하나의 배열이나 객체로 모읍니다.
분해 구문의 마지막 자리에만 사용할 수 있습니다.
함수의 매개변수 자리에 사용하면 나머지 매개변수(Rest Parameter)라고 하며, 전달된 인자를 배열로 모읍니다.

JS
1
2
3
4
5
6
7
8
9
10
11
const [head, ...tail] = [1, 2, 3, 4] // head는 1, tail은 [2, 3, 4] const { id, ...rest } = { id: 1, name: 'Kim', age: 20 } // id는 1, rest는 { name: 'Kim', age: 20 } function sum(...numbers) { // 인자들을 배열로 모음, numbers는 [1, 2, 3] let total = 0 for (const n of numbers) total += n return total } sum(1, 2, 3) // 6

객체에서 특정 속성만 제외한 복사본을 만들 때 나머지 속성을 자주 사용합니다.

JS
1
const { password, ...safeUser } = user // password를 뺀 나머지만 safeUser에

# 배열 메소드

반복문 대신 배열 메소드로 데이터를 다루는 경우가 많습니다.
모두 콜백 함수를 인자로 받으며, 콜백은 요소, 인덱스, 배열 순서의 인자를 받습니다.

map은 개수를 유지한 채 요소를 바꾸고, filter는 일부만 남기며, reduce는 하나로 합칩니다

메소드 동작 반환
map 각 요소를 변환 같은 길이의 새 배열
filter 조건을 만족하는 요소만 추림 새 배열
find 조건을 처음 만족하는 요소 요소 하나, 없으면 undefined
findIndex 조건을 처음 만족하는 요소의 위치 인덱스, 없으면 -1
some 하나라도 만족하는지 true 또는 false
every 모두 만족하는지 true 또는 false
includes 특정 값이 있는지 true 또는 false
reduce 요소를 누적해 하나로 합침 누적 결과
forEach 요소마다 동작만 실행 없음(undefined)
JS
1
2
3
4
5
6
7
8
9
10
11
12
const users = [ { name: 'Kim', age: 20, active: true }, { name: 'Lee', age: 17, active: false }, { name: 'Park', age: 25, active: true } ] const names = users.map(u => u.name) // ['Kim', 'Lee', 'Park'] const adults = users.filter(u => u.age >= 20) // Kim, Park const lee = users.find(u => u.name === 'Lee') // { name: 'Lee', ... } const hasActive = users.some(u => u.active) // true const allAdults = users.every(u => u.age >= 20) // false const totalAge = users.reduce((sum, u) => sum + u.age, 0) // 62

reduce는 누적값과 현재 요소를 받는 콜백과, 누적값의 초깃값을 인자로 받습니다.
합계뿐 아니라 배열을 객체로 바꾸는 등 형태를 바꾸는 작업에도 사용합니다.

JS
1
2
3
4
const byName = users.reduce((acc, u) => { acc[u.name] = u // 이름을 키로 하는 객체로 변환 return acc }, {}) // { Kim: {...}, Lee: {...}, Park: {...} }

새 배열을 반환하는 메소드는 점(.)으로 이어서 호출할 수 있습니다.
이것을 메소드 체이닝(Method Chaining)이라고 합니다.

JS
1
2
3
const activeNames = users .filter(u => u.active) .map(u => u.name) // ['Kim', 'Park']

정렬은 sort나 toSorted로 합니다.
sort는 원본 배열을 그 자리에서 정렬하고, toSorted는 원본을 그대로 둔 채 정렬된 새 배열을 반환합니다.
둘 다 기본적으로 요소를 문자열로 바꿔서 비교하기 때문에, 숫자를 정렬할 때는 비교 함수를 전달해야 합니다.
비교 함수가 음수를 반환하면 첫 번째 인자가 앞에 옵니다.

JS
1
2
3
4
5
const nums = [10, 9, 1] nums.toSorted() // [1, 10, 9], 문자열 기준 정렬 nums.toSorted((a, b) => a - b) // [1, 9, 10], 오름차순 users.toSorted((a, b) => b.age - a.age) // 나이 내림차순

toSorted, toReversed, toSpliced, with는 ES2023에 추가된 메소드로, 최신 브라우저와 Node.js 20 이상에서 사용할 수 있습니다.

반복문이 필요할 때는 for...of를 사용합니다.
forEach와 달리 break로 중간에 멈출 수 있고, 안에서 await를 사용할 수 있습니다.

JS
1
2
3
4
5
6
7
8
for (const user of users) { if (!user.active) continue console.log(user.name) } for (const [index, user] of users.entries()) { console.log(index, user.name) // 인덱스가 필요할 때 }

# 불변성과 복사

원시 타입의 값은 불변(Immutability)입니다.
값을 바꾸는 것처럼 보여도 기존 값이 변하는 것이 아니라, 변수가 새 값을 가리킬 뿐입니다.

객체와 배열은 가변(Mutability)입니다.
참조는 그대로 둔 채 내부 속성이나 요소를 바꿀 수 있습니다.
const는 재할당만 제한할 뿐 내부 변경까지 막지는 않습니다.

JS
1
2
3
const user = { name: 'Kim' } user.name = 'Lee' // 가능, 내부 값 변경 // user = {} // 에러, 재할당

배열 메소드는 원본을 바꾸는 것과 새 배열을 반환하는 것으로 나뉩니다.

구분 메소드
원본을 바꿈 push, pop, shift, unshift, splice, sort, reverse
새 배열을 반환 map, filter, slice, concat, toSorted, toReversed, toSpliced, with

sort와 reverse는 정렬된 새 배열을 반환하는 것이 아니라 원본을 그 자리에서 바꾸기 때문에 특히 실수가 잦습니다.
원본을 유지하려면 복사본을 정렬하거나, 새 배열을 반환하는 toSorted를 사용합니다.

JS
1
2
3
4
5
const nums = [3, 1, 2] nums.sort() // 원본이 바뀜, nums는 [1, 2, 3] const sorted = [...nums].sort() // 복사본만 정렬 const sorted2 = nums.toSorted() // 새 배열 반환, 원본 유지

원본을 변경하지 않고 값을 바꾸는 방법입니다.
객체나 배열을 복사한 뒤 필요한 부분만 바꾼 새 값을 만듭니다.

JS
1
2
3
4
5
6
7
const user = { name: 'Kim', age: 20 } const updated = { ...user, age: 21 } // 복사 후 일부만 변경 const list = [1, 2, 3] const added = [...list, 4] // 추가, [1, 2, 3, 4] const removed = list.filter(n => n !== 2) // 삭제, [1, 3] const doubled = list.map(n => n * 2) // 변경, [2, 4, 6]

이렇게 다루면 값이 언제 바뀌는지 추적하기 쉽고, 공유된 참조를 실수로 바꾸는 버그를 줄일 수 있습니다.
React처럼 참조가 바뀌었는지로 변경을 감지하는 도구에서는, 원본을 직접 바꾸면 값이 바뀌었는데도 화면이 갱신되지 않는 문제가 생깁니다.

전개 연산자와 Object.assign은 얕은 복사(Shallow Copy)입니다.
1단계 속성만 복사하고, 그 안의 객체는 여전히 같은 참조를 공유합니다.

얕은 복사는 바깥 객체만 새로 만들고 안쪽 객체는 공유하며, 깊은 복사는 안쪽까지 새로 만듭니다

JS
1
2
3
4
5
const origin = { info: { age: 20 } } const copy = { ...origin } copy.info.age = 30 console.log(origin.info.age) // 30, 안쪽 객체는 공유됨

중첩 구조까지 완전히 복사하려면 깊은 복사(Deep Copy)를 사용합니다.
structuredClone은 중첩된 객체와 배열을 모두 새로 만듭니다.

JS
1
2
3
4
const deep = structuredClone(origin) // 안쪽 객체까지 별도로 복사 deep.info.age = 40 console.log(origin.info.age) // 30, 원본은 그대로

중첩된 값을 원본 변경 없이 바꿀 때는 바꾸려는 단계마다 전개 연산자로 복사합니다.

JS
1
2
3
4
const next = { ...origin, info: { ...origin.info, age: 40 } // 안쪽 객체도 새로 만듦 }

# 객체 다루기

속성 이름과 변수 이름이 같으면 한 번만 작성합니다(단축 속성, Shorthand Property).
메소드도 function 키워드 없이 축약해서 작성합니다(메소드 축약, Method Shorthand).

JS
1
2
3
4
5
6
7
8
9
10
const name = 'Kim' const age = 20 const user = { name, // name: name age, greet() { // greet: function () { ... } return `안녕하세요, ${this.name}입니다` } }

속성 이름을 대괄호로 감싸면 변수의 값을 속성 이름으로 사용합니다(계산된 속성명, Computed Property Name).
입력 필드의 이름으로 상태를 갱신하는 것처럼, 속성 이름이 실행 중에 정해질 때 사용합니다.

JS
1
2
3
4
5
6
7
const key = 'score' const obj = { [key]: 100 } // { score: 100 } const form = { email: '', password: '' } function update(field, value) { return { ...form, [field]: value } // 어떤 필드든 같은 함수로 갱신 }

객체에는 map, filter 같은 배열 메소드가 없어서, 속성을 배열로 변환한 뒤에 다룹니다.
Object.entries로 배열을 만들고, 가공한 뒤 Object.fromEntries로 다시 객체로 만들 수 있습니다.

JS
1
2
3
4
5
6
7
8
9
10
11
12
13
const scores = { kim: 90, lee: 85 } Object.keys(scores) // ['kim', 'lee'] Object.values(scores) // [90, 85] Object.entries(scores) // [['kim', 90], ['lee', 85]] for (const [name, score] of Object.entries(scores)) { console.log(`${name}: ${score}`) // 'kim: 90', 'lee: 85' } const passed = Object.fromEntries( Object.entries(scores).filter(([, score]) => score >= 90) ) // { kim: 90 }

객체는 그대로 서버에 보내거나 저장할 수 없으므로 문자열로 바꿔서 주고받습니다.
JSON(JavaScript Object Notation)은 이때 사용하는 텍스트 형식이고, JSON.stringify와 JSON.parse로 변환합니다.

JS
1
2
3
4
5
const json = JSON.stringify({ name: 'Kim', age: 20 }) // '{"name":"Kim","age":20}' const user = JSON.parse(json) // { name: 'Kim', age: 20 } localStorage.setItem('user', JSON.stringify(user)) // 저장할 때 문자열로 const saved = JSON.parse(localStorage.getItem('user')) // 꺼낼 때 다시 객체로

JSON.stringify는 함수와 undefined 속성을 버리고, Date 객체를 문자열로 바꿉니다.
변환 뒤에 다시 JSON.parse를 해도 원래 타입으로 돌아오지 않는다는 점에 주의합니다.

Set은 중복 없는 값의 모음이고, Map은 어떤 타입이든 키로 사용할 수 있는 키-값 저장소입니다.
배열의 중복을 제거할 때 Set을 가장 자주 사용합니다.

JS
1
2
3
4
5
6
7
8
9
10
11
12
const tags = ['js', 'ts', 'js'] const unique = [...new Set(tags)] // ['js', 'ts'] const set = new Set([1, 2]) set.add(3) set.has(2) // true set.size // 3 const map = new Map() map.set('kim', { age: 20 }) map.set(button, 'clicked') // 객체도 키로 사용 가능 map.get('kim') // { age: 20 }

# 비동기 처리

# 동기와 비동기

동기(Synchronous) 코드는 위에서 아래로 한 줄씩 실행되며, 앞의 작업이 끝나야 다음으로 넘어갑니다.
비동기(Asynchronous)는 네트워크 요청이나 타이머처럼 시간이 걸리는 작업을 시작만 한 뒤, 끝나기를 기다리지 않은 채 다음 코드를 먼저 실행하는 방식입니다.
작업 결과는 나중에 콜백이나 Promise로 받습니다.

동기는 앞 작업이 끝나야 다음으로 넘어가고, 비동기는 기다리는 동안 다음 코드를 먼저 실행합니다

JS
1
2
3
4
console.log(1) setTimeout(() => console.log(2), 0) // 0초 뒤라도 나중에 실행 console.log(3) // 출력 순서: 1, 3, 2

JavaScript는 코드를 한 줄씩 실행하는 하나의 흐름만 가지므로, 시간이 걸리는 작업을 동기로 기다리면 그동안 화면이 멈춥니다.
그래서 브라우저의 네트워크 요청, 파일 읽기, 타이머는 모두 비동기로 동작합니다.

# Promise

Promise는 비동기 작업의 결과를 나중에 담을 것을 약속하는 객체입니다.
세 가지 상태를 가지며, 대기에서 한 번 이행이나 거부로 정해지면 다시 바뀌지 않습니다.

Promise는 대기 상태에서 이행이나 거부로 한 번만 바뀝니다

  • 대기(Pending): 아직 끝나지 않음.
  • 이행(Fulfilled): 성공해서 결과값이 생김.
  • 거부(Rejected): 실패해서 에러가 생김.

결과는 then, 에러는 catch, 성공 여부와 무관한 마무리는 finally로 받습니다.
아래 예제의 fetchUser와 fetchPosts는 서버에서 데이터를 가져와서 Promise를 반환하는 함수라고 가정합니다.

JS
1
2
3
4
fetchUser() .then(user => console.log(user.name)) // 이행되면 실행 .catch(error => console.log(error.message)) // 거부되면 실행 .finally(() => console.log('요청 종료')) // 항상 실행

then은 새 Promise를 반환하므로 연달아 이어 붙일 수 있습니다.
콜백에서 반환한 값이 다음 then으로 전달되고, 체인 중간에 에러가 발생하면 가장 가까운 catch로 넘어갑니다.

JS
1
2
3
4
fetchUser() .then(user => fetchPosts(user.id)) // 앞 결과로 다음 작업 수행 .then(posts => console.log(posts)) .catch(error => console.log(error)) // 어느 단계에서 실패해도 여기로

직접 만들 때는 new Promise에 성공용 resolve와 실패용 reject를 받는 함수를 전달합니다.
콜백 방식의 API를 Promise로 감쌀 때 주로 사용합니다.

JS
1
2
3
4
5
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } delay(1000).then(() => console.log('1초 후 실행'))

# async/await

async와 await는 Promise를 동기 코드처럼 위에서 아래로 읽히게 작성하는 문법입니다.

  • async 함수는 항상 Promise를 반환합니다.
  • await는 Promise가 이행될 때까지 기다린 뒤에 결과값을 돌려줍니다.
  • await는 async 함수 안에서 사용합니다. 모듈의 최상위에서도 사용할 수 있습니다.
  • await는 그 함수의 실행만 잠시 멈출 뿐, 프로그램 전체를 멈추지 않습니다.
JS
1
2
3
4
5
6
7
async function load() { const user = await fetchUser() const posts = await fetchPosts(user.id) // user를 받은 뒤 실행 return posts // Promise로 감싸져 반환 } load().then(posts => console.log(posts))

Promise가 거부되면 await는 그 자리에서 에러를 던집니다.
try...catch로 감싸서 처리하며, 성공 여부와 무관하게 실행할 코드는 finally에 작성합니다.

JS
1
2
3
4
5
6
7
8
9
10
11
async function load() { try { const user = await fetchUser() return user } catch (error) { console.log('불러오기 실패:', error.message) return null } finally { setLoading(false) // 성공하든 실패하든 실행 } }

# fetch

fetch는 브라우저와 Node.js에 내장된 네트워크 요청 함수입니다.
응답 객체를 담은 Promise를 반환하고, 본문을 JSON으로 읽는 json()도 Promise를 반환하므로 두 번 await합니다.

JS
1
2
3
4
5
6
7
async function getUser(id) { const response = await fetch(`/api/users/${id}`) if (!response.ok) { // 404, 500 같은 HTTP 에러는 거부되지 않으므로 직접 확인 throw new Error(`요청 실패: ${response.status}`) } return await response.json() // 본문을 객체로 변환 }

fetch는 네트워크 자체가 실패했을 때만 거부되고, 서버가 에러 상태 코드를 보내도 이행됩니다.
그래서 response.ok를 확인하는 코드가 필요합니다.

데이터를 보낼 때는 method, headers, body를 옵션으로 전달합니다.
객체는 JSON.stringify로 문자열로 바꿔서 보냅니다.

JS
1
2
3
4
5
const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Kim', age: 20 }) })

# 여러 작업 동시 처리

서로 무관한 작업을 차례대로 await하면 앞 작업이 끝날 때까지 다음 작업을 시작하지 못합니다.
Promise.all에 배열로 전달하면 모든 작업을 동시에 시작하고, 전부 끝났을 때 결과를 배열로 받습니다.

차례로 기다리면 시간이 더해지고, Promise.all로 동시에 시작하면 가장 긴 작업 시간만 걸립니다

JS
1
2
const user = await fetchUser() // 1초 const config = await fetchConfig() // 1초, 앞 작업이 끝난 뒤 시작
차례로 기다림, 총 2초
JS
1
const [user, config] = await Promise.all([fetchUser(), fetchConfig()])
동시에 시작, 총 1초

여러 Promise를 함께 다루는 메소드는 실패를 다루는 방식이 다릅니다.

메소드 동작
Promise.all 모두 성공하면 결과 배열, 하나라도 실패하면 즉시 거부
Promise.allSettled 성공과 실패에 상관없이 모든 결과를 받음
Promise.race 가장 먼저 끝난 하나의 결과, 성공이든 실패든
Promise.any 가장 먼저 성공한 하나의 결과
JS
1
2
3
4
5
6
const results = await Promise.allSettled([fetchUser(), fetchConfig()]) for (const result of results) { if (result.status === 'fulfilled') console.log(result.value) // 성공한 결과 else console.log(result.reason) // 실패한 이유 }

forEach 안에서 await를 사용하면 기다리지 않고 다음 요소로 넘어갑니다.
순서대로 기다려야 하면 for...of를, 동시에 실행해도 되면 map으로 Promise 배열을 만들어 Promise.all에 전달합니다.

JS
1
2
3
4
5
for (const id of ids) { await deleteUser(id) // 하나씩 순서대로 } await Promise.all(ids.map(id => deleteUser(id))) // 모두 동시에

# 모듈

모듈(Module)은 코드를 여러 파일로 나누고 import와 export로 연결하는 구조입니다.
파일 하나가 모듈이며, 각 모듈은 독립된 스코프를 가져서 export하지 않은 변수는 밖에서 접근할 수 없습니다.

export한 것만 다른 파일에서 import로 가져올 수 있습니다

내보내는 방법은 두 가지입니다.

  • 이름 있는 내보내기(Named Export): 이름을 붙여 여러 개를 내보냅니다. 가져올 때 같은 이름을 중괄호로 받습니다.
  • 기본 내보내기(Default Export): 파일당 하나만 내보냅니다. 가져올 때 이름을 자유롭게 정합니다.
/math.js
JS
1
2
3
4
5
6
export const add = (a, b) => a + b // 이름 있는 내보내기 export const PI = 3.14 export default function multiply(a, b) { // 기본 내보내기 return a * b }
/main.js
JS
1
2
3
import multiply, { add, PI } from './math.js' // 기본과 이름 함께 import { add as plus } from './math.js' // as로 이름 변경 import * as math from './math.js' // 전부 묶어서 math.add 형태로 사용

경로 작성 규칙입니다.

  • ./나 ../로 시작하면 직접 작성한 파일을 가리킵니다.
  • 이름만 작성하면 설치한 패키지(node_modules)를 가리킵니다.
JS
1
2
import { format } from 'date-fns' // 설치한 패키지 import { add } from './math.js' // 직접 작성한 파일

브라우저에서 직접 사용할 때는 <script type="module">로 불러옵니다.
Vite 같은 빌드 도구를 사용하는 프로젝트에서는 별도 설정 없이 사용하며, 빌드 과정에서 여러 모듈이 하나의 파일로 합쳐집니다.

import()를 함수처럼 호출하면 필요한 시점에 모듈을 불러옵니다.
초기 로딩 크기를 줄이기 위해 무거운 모듈을 나중에 불러올 때 사용합니다.

JS
1
2
3
4
button.addEventListener('click', async () => { const { default: Chart } = await import('./chart.js') // 클릭 시점에 불러옴 new Chart() })

# 클래스

클래스(Class)는 같은 구조의 객체를 여러 개 만들기 위한 틀이고, 클래스로 만든 객체를 인스턴스(Instance)라고 합니다.
new로 인스턴스를 만들고, constructor에서 초기 상태를 정하며, 메소드에서 this로 인스턴스 자신에 접근합니다.

JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter { count = 0 // 필드 constructor(start) { this.count = start } increase() { // 메소드 this.count += 1 return this.count } } const counter = new Counter(10) counter.increase() // 11 counter instanceof Counter // true, Counter로 만든 인스턴스인지 확인

extends로 기존 클래스를 확장합니다.
직접 클래스를 설계하는 일보다, 내장 클래스나 라이브러리의 클래스를 확장하거나 인스턴스를 만드는 일이 더 많습니다.
Error를 확장한 클래스는 에러의 종류를 구분할 때 자주 사용합니다.

JS
1
2
3
4
5
6
7
8
9
10
11
12
class HttpError extends Error { constructor(status, message) { super(message) // 부모 클래스의 constructor 호출 this.status = status } } try { throw new HttpError(404, '찾을 수 없음') } catch (error) { if (error instanceof HttpError) console.log(error.status) // 404 }

프로토타입, 게터와 세터, 정적 메소드 등은 JS 클래스 핵심 패턴에서 다룹니다.

# TypeScript

TypeScript는 JavaScript에 타입(Type)을 추가한 언어입니다.
모든 JavaScript 코드는 유효한 TypeScript 코드이므로, 지금까지 살펴본 문법은 그대로 사용하고 타입 문법만 추가로 익히면 됩니다.

TypeScript 코드는 타입 검사를 거친 뒤 타입이 제거된 JavaScript로 변환됩니다

  • 잘못된 값 전달이나 오타를 실행 전(편집기, 빌드 단계)에 확인할 수 있습니다.
  • 자동 완성과 코드 추적이 정확해집니다.
  • 타입은 JavaScript로 변환될 때 사라지므로 실행 속도에는 영향이 없습니다.
TS
1
2
let count: number = 0 count = '3' // 편집기에서 바로 에러로 표시됨
JS
1
2
let count = 0 count = '3' // 타입이 사라지고 코드만 남음
변환된 JavaScript

파일 확장자는 .ts를 사용하고, React처럼 JSX를 사용하는 파일은 .tsx를 사용합니다.
Vite로 만든 프로젝트는 별도 설정 없이 .ts 파일을 사용할 수 있으며, 다른 개발 환경 구성은 한눈에 보는 타입스크립트를 참고하세요.

타입은 실행 전 검사일 뿐, 실행 중에 값을 확인하지 않습니다.
서버 응답처럼 외부에서 들어오는 값은 타입을 작성해도 실제 값이 다를 수 있으며, 이 내용은 마지막의 외부 데이터의 타입에서 다룹니다.

# 타입 지정과 추론

타입 지정(Type Annotation)은 변수나 함수에 "이 값은 이런 타입이다"라고 명시하는 것입니다.
이름 뒤에 콜론(:)을 붙이고 타입을 작성합니다.

TS
1
2
3
let title: string = 'Hello' let age: number = 20 let isOpen: boolean = false

기본 타입은 소문자로 작성합니다.

타입 값
string 문자열
number 정수와 실수 모두
boolean true, false
null, undefined 각각 그 값 하나만 허용
타입[] 그 타입의 배열. number[], string[]
{ 속성: 타입 } 객체. { id: number; name: string }
TS
1
2
3
4
5
const ids: number[] = [1, 2, 3] const names: string[] = ['Kim', 'Lee'] const user: { id: number; name: string } = { id: 1, name: 'Kim' } const users: { id: number; name: string }[] = [] // 객체의 배열

함수는 매개변수와 반환값에 타입을 작성합니다.
반환값이 없으면 void를 사용합니다.

TS
1
2
3
4
5
6
7
function add(a: number, b: number): number { return a + b } const greet = (name: string): void => { console.log(`${name}님`) }

초깃값이 있으면 타입을 지정하지 않아도 타입이 자동으로 추론(Type Inference)됩니다.
편집기에서 변수에 마우스를 올리면 추론된 타입을 확인할 수 있습니다.

TS
1
2
3
4
5
6
7
let count = 0 // number로 추론 let text = 'hi' // string으로 추론 const user = { id: 1, name: 'Kim' } // { id: number; name: string }으로 추론 function double(x: number) { return x * 2 // 반환 타입이 number로 추론 }

추론되는 곳은 추론에 맡기고, 추론이 안 되거나 모호한 곳에만 직접 지정하는 것이 실용적입니다.
함수의 매개변수는 추론할 근거가 없으므로 항상 작성하고, 빈 배열처럼 초깃값만으로는 나중에 들어올 값의 타입을 알 수 없을 때도 작성합니다.

TS
1
2
const items: string[] = [] // 빈 배열은 요소 타입을 알 수 없으므로 작성 items.push('a') // string만 추가 가능

# 유니언과 리터럴 타입

유니언 타입(Union Type)은 |로 여러 타입 중 하나를 허용합니다.
리터럴 타입(Literal Type)은 정해진 특정 값만 허용합니다.
둘을 합치면 허용하는 값의 목록을 타입으로 표현할 수 있어서, 문자열 상태값이나 옵션을 정의할 때 매우 자주 사용합니다.

TS
1
2
3
4
5
6
7
8
9
let value: string | number // 문자열 또는 숫자 value = 'hello' value = 42 let status: 'idle' | 'loading' | 'done' // 이 세 값만 허용 status = 'loading' // status = 'stop' // 에러 let user: User | null = null // 비어 있을 수 있는 값, User는 다음 절에서 정의하는 객체 타입

type 키워드로 타입에 이름을 붙인 것을 타입 별칭(Type Alias)이라고 하며, 여러 곳에서 재사용할 수 있습니다.

TS
1
2
3
4
5
6
type Status = 'idle' | 'loading' | 'done' type Size = 'sm' | 'md' | 'lg' function setSize(size: Size) {} setSize('md') // setSize('xl') // 에러

값을 나열한 배열에서 타입을 뽑아내려면 Const 단언(Const Assertion) as const로 값을 리터럴 타입으로 고정합니다.
같은 목록을 값과 타입 양쪽에서 사용할 수 있습니다.

TS
1
2
3
4
const SIZES = ['sm', 'md', 'lg'] as const // readonly ['sm', 'md', 'lg'] type Size = (typeof SIZES)[number] // 'sm' | 'md' | 'lg' SIZES.map(size => `<option>${size}</option>`) // 값으로도 사용

typeof SIZES는 배열 값의 타입이고, 뒤에 [number]를 붙이면 그 배열 요소의 타입을 꺼냅니다.

TypeScript에는 enum도 있지만, 웹 개발에서는 위와 같은 리터럴 유니언을 더 많이 사용합니다.
enum의 특징과 차이는 타입스크립트 Enum 타입 자세히 보기를 참고하세요.

# interface와 type

객체의 모양을 정의하는 두 가지 방법입니다.
interface는 객체 구조를 정의하며, extends로 다른 인터페이스를 물려받아 확장할 수 있습니다.

TS
1
2
3
4
5
6
7
8
9
10
interface User { id: number name: string } interface Admin extends User { // User의 속성을 물려받아 확장 role: string } const admin: Admin = { id: 1, name: 'Kim', role: 'owner' }

타입 별칭을 만드는 type은 객체뿐 아니라 유니언, 함수, 원시 타입 등 어떤 타입에든 이름을 붙일 수 있습니다.
객체 타입을 합칠 때는 인터섹션 타입(Intersection Type) &를 사용합니다.

TS
1
2
3
type ID = number | string // 유니언에 이름 붙이기 type Handler = (event: MouseEvent) => void // 함수 타입 type Admin = User & { role: string } // 두 객체 타입 합치기

객체 타입은 interface로 정의하고, 유니언처럼 interface로 표현할 수 없는 타입이나 함수 타입에 type을 사용합니다.
공식 문서도 type이 필요한 상황이 되기 전까지는 interface를 사용하라고 안내합니다.
interface는 같은 이름을 여러 번 선언하면 하나로 합쳐지는 선언 병합(Declaration Merging)이 가능해서, 라이브러리가 제공하는 타입에 속성을 추가할 때도 유용합니다.
이 내용은 타입스크립트에서 interface와 type 중에 뭘 사용해야 하나요?에서 자세히 다룹니다.

속성 이름 뒤에 ?를 붙이면 있어도 되고 없어도 되는 선택적 속성(Optional Property)이 됩니다.
타입은 자동으로 값 | undefined가 되므로, 사용할 때는 값이 있는지 확인해야 합니다.
readonly를 붙이면 한 번 정해진 뒤 다시 바꿀 수 없습니다.

TS
1
2
3
4
5
6
7
8
9
interface Props { title: string subtitle?: string // 없어도 됨, string | undefined readonly id: number // 읽기 전용 } const props: Props = { title: '제목', id: 1 } props.subtitle?.toUpperCase() // 없을 수 있으므로 ?.로 접근 // props.id = 2 // 에러, readonly

함수 타입은 화살표 함수와 비슷한 형태로 작성합니다.
이벤트 핸들러나 콜백을 props로 전달할 때 사용합니다.

TS
1
2
3
4
5
6
7
8
interface ButtonProps { label: string onClick: (event: MouseEvent) => void // 함수 타입 onChange?: (value: string) => void // 선택적 속성 } type Calculator = (a: number, b: number) => number const sum: Calculator = (a, b) => a + b // 매개변수 타입은 Calculator에서 추론

타입만 가져올 때는 import type을 사용합니다.
JavaScript로 변환될 때 해당 import가 제거되므로, 타입 전용 파일을 가져올 때 불필요한 코드가 남지 않습니다.

TS
1
2
import type { User } from './types' // 타입만 가져옴 import { fetchUser, type Role } from './api' // 값과 타입을 함께 가져올 때는 항목 앞에 type

# 타입 가드

유니언 타입처럼 여러 타입이 될 수 있는 값은 그 타입 모두에서 가능한 동작만 허용됩니다.
타입 가드(Type Guard)는 typeof나 instanceof처럼 값의 타입을 확인하는 조건이고, 가드를 통과한 블록 안에서는 타입이 하나로 좁혀져서(Narrowing) 해당 타입에서만 가능한 동작을 사용할 수 있습니다.

조건으로 확인한 블록 안에서는 유니언 타입이 하나의 타입으로 좁혀집니다

타입 가드 용도
typeof 값 === 'string' 원시 타입 구분
Array.isArray(값) 배열인지 확인
'속성' in 값 객체에 특정 속성이 있는지 확인
값 instanceof 클래스 특정 클래스의 인스턴스인지 확인
값 === null, !값 null과 undefined 제외
TS
1
2
3
4
5
6
function format(value: string | number) { if (typeof value === 'string') { return value.toUpperCase() // 이 블록 안에서는 string } return value.toFixed(2) // 여기서는 number }

null이 될 수 있는 값은 먼저 확인하고 빠져나가는 패턴을 자주 사용합니다.
확인한 뒤의 코드에서는 null이 제외된 타입으로 다룹니다.

TS
1
2
3
4
function getName(user: User | null) { if (!user) return '이름 없음' // null이면 먼저 반환 return user.name // 이 아래에서는 User로 확정 }

객체 유니언은 공통 속성의 리터럴 값으로 구분합니다.
성공과 실패처럼 형태가 다른 결과를 하나의 타입으로 표현하고, 기준이 되는 판별 속성(Discriminant)을 확인하면 나머지 속성의 타입이 함께 좁혀집니다.
이것을 구분된 유니언(Discriminated Union)이라고 하며, API 응답이나 상태 표현에 자주 사용합니다.
자세한 내용은 한눈에 보는 타입스크립트에서 다룹니다.

TS
1
2
3
4
5
6
7
8
9
10
11
type Result = | { ok: true; data: User } | { ok: false; error: string } function handle(result: Result) { if (result.ok) { console.log(result.data.name) // ok가 true인 형태로 좁혀져 data 사용 가능 } else { console.log(result.error) // 여기서는 error만 존재 } }

# any, unknown, never

세 타입은 각각 "무엇이든", "모름", "없음"을 나타냅니다.

unknown은 모든 값을 담고, never는 어떤 값도 없으며, any는 검사를 끕니다

  • any: 타입 검사를 하지 않습니다. 무엇이든 허용하지만 TypeScript의 장점을 사용할 수 없으므로 피합니다.
  • unknown: 무엇인지 모르는 값입니다. any와 달리 사용하기 전에 타입 확인을 강제하므로 안전합니다.
  • never: 어떤 값도 될 수 없습니다. 항상 에러를 던지거나 끝나지 않는 함수의 반환 타입으로 사용합니다.
TS
1
2
3
4
5
6
7
8
9
10
11
12
let a: any = JSON.parse(text) a.foo.bar // 검사하지 않음, 실행 중에 에러 가능 let u: unknown = JSON.parse(text) // u.name // 에러, 확인 전에는 사용 불가 if (typeof u === 'object' && u !== null && 'name' in u) { console.log(u.name) // 좁힌 뒤에 사용 } function fail(message: string): never { throw new Error(message) // 정상적으로 값을 반환하지 않음 }

unknown은 외부에서 들어온 값을 받을 때 any 대신 사용합니다.
catch의 error도 어떤 값이든 던져질 수 있어서 unknown이므로, Error 인스턴스인지 확인한 뒤에 message를 읽습니다.

TS
1
2
3
4
5
6
try { await fetchUser() } catch (error) { // tsconfig의 strict 옵션이 켜져 있으면 error는 unknown const message = error instanceof Error ? error.message : String(error) console.log(message) }

# 타입 단언

타입 단언(Type Assertion)은 "이 값의 타입은 내가 더 정확히 안다"고 TypeScript에 알려 주는 문법입니다.
실제 값을 바꾸거나 확인하지는 않으며, 단언이 틀리면 실행 중 오류로 이어지므로 꼭 필요할 때만 사용합니다.

as는 값의 타입을 단언합니다.
DOM 요소처럼 구체적인 타입을 추론할 수 없는 값에 주로 사용합니다.

TS
1
2
3
4
5
// getElementById의 반환 타입은 HTMLElement | null이라 value 속성이 없음 const input = document.getElementById('email') as HTMLInputElement input.value = 'test@mail.com' // HTMLInputElement로 단언했으니 value 사용 가능 const data = JSON.parse(text) as User // 파싱 결과를 User로 간주, 실제 값은 확인하지 않음

!는 이 값이 null이나 undefined가 아니라고 단언하는 Non-null 단언 연산자(Non-null Assertion Operator)입니다.

TS
1
2
const el = document.querySelector('input') // 타입: HTMLInputElement | null el!.focus() // null이 아니라고 단언, 확신할 때만 사용

가능하면 타입 가드로 먼저 확인하고, 단언은 마지막 수단으로 사용합니다.

TS
1
2
const el = document.querySelector('input') if (el) el.focus() // 확인 뒤 사용, 없을 때도 안전

# 제네릭

제네릭(Generic)은 타입을 나중에 정하도록 자리를 비워 두고, 사용할 때 채우는 문법입니다.
<T>처럼 꺾쇠 안에 타입 매개변수를 선언하고, 호출할 때 타입을 전달하거나 인자에서 추론합니다.
함수의 인자처럼 타입을 전달한다고 생각하면 이해하기 쉽습니다.

TS
1
2
3
4
5
6
function first<T>(arr: T[]): T { return arr[0] } const n = first<number>([1, 2, 3]) // T가 number, 반환도 number const s = first(['a', 'b']) // 인자에서 T를 string으로 추론

직접 만드는 일보다, 라이브러리가 요구하는 < > 자리에 타입을 작성하는 일이 훨씬 많습니다.
Promise<User>는 "User로 이행되는 Promise", useState<User | null>은 "User 또는 null을 담는 상태"라는 뜻입니다.

TS
1
2
3
4
async function getUser(id: number): Promise<User> { // User를 담은 Promise const response = await fetch(`/api/users/${id}`) return response.json() }
TS
1
const [user, setUser] = useState<User | null>(null) // 상태에 담을 타입 전달
React
TS
1
const user = ref<User | null>(null)
Vue

객체 타입에도 제네릭을 사용합니다.
API 응답처럼 겉 구조는 같고 안에 담긴 데이터만 다른 경우에 사용합니다.

TS
1
2
3
4
5
6
7
interface ApiResponse<T> { data: T status: number } const userResponse: ApiResponse<User> = { data: { id: 1, name: 'Kim' }, status: 200 } const listResponse: ApiResponse<User[]> = { data: [], status: 200 }

extends로 타입 매개변수에 제약 조건(Constraint)을 둘 수 있습니다.
"어떤 타입이든 되지만 최소한 이 구조는 있어야 한다"는 뜻입니다.

TS
1
2
3
4
5
6
function getId<T extends { id: number }>(item: T) { return item.id // id가 있다고 보장되므로 사용 가능 } getId({ id: 1, name: 'Kim' }) // getId({ name: 'Kim' }) // 에러, id가 없음

# 유틸리티 타입

유틸리티 타입(Utility Type)은 기존 타입에서 새 타입을 만드는 내장 제네릭 타입입니다.
비슷한 타입을 여러 번 정의하는 대신, 하나의 타입에서 변형을 만듭니다.

유틸리티 타입 결과
Partial<T> 모든 속성을 선택적으로
Required<T> 모든 속성을 필수로
Pick<T, K> 지정한 속성만 선택
Omit<T, K> 지정한 속성만 제외
Record<K, V> K를 키, V를 값으로 하는 객체
ReturnType<F> 함수 F의 반환 타입
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
interface User { id: number name: string email: string } type UserUpdate = Partial<User> // { id?: number; name?: string; email?: string } type UserPreview = Pick<User, 'id' | 'name'> // { id: number; name: string } type UserForm = Omit<User, 'id'> // id를 뺀 나머지, 생성 폼에 사용 type UsersById = Record<number, User> // 숫자 키에 User를 담는 객체 function update(id: number, changes: UserUpdate) {} update(1, { name: 'Lee' }) // 바꿀 속성만 전달

전체 목록과 동작 원리는 타입스크립트 유틸리티 타입 자세히 보기에서 다룹니다.

# 외부 데이터의 타입

타입은 실행 전에만 검사되고 실행 중에는 사라지므로, 서버 응답이나 localStorage에서 읽은 값처럼 외부에서 들어오는 데이터는 타입 검사의 대상이 아닙니다.
as User로 단언하거나 Promise<User>로 작성하는 것은 "그렇다고 믿겠다"는 선언일 뿐, 실제 값이 다르면 실행 중에 문제가 생깁니다.

TS
1
2
const user = (await response.json()) as User // 서버가 다른 형태를 보내도 검사되지 않음 user.name.toUpperCase() // name이 없으면 실행 중 에러

값이 확실히 필요한 곳에서는 unknown으로 받고 실제 값을 확인한 뒤에 사용합니다.
반환 타입에 value is User처럼 작성하는 타입 술부(Type Predicate)를 사용하면 그 함수가 타입 가드가 되어, true를 반환했을 때 호출한 쪽에서도 타입이 좁혀집니다.

TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function isUser(value: unknown): value is User { // 참이면 value를 User로 좁힘 return ( typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'number' && 'name' in value && typeof value.name === 'string' ) } const data: unknown = await response.json() if (isUser(data)) { console.log(data.name) // 확인된 뒤에는 User로 사용 }

확인할 속성이 많아지면 직접 작성하는 대신 스키마로 검증하는 라이브러리를 사용합니다.
Zod 핵심 정리에서 스키마 하나로 타입과 검증을 함께 정의하는 방법을 다룹니다.

# 다음 단계

문법이 어느 정도 익숙해졌다면 사용할 프레임워크로 넘어갑니다.
프레임워크마다 컴포넌트를 작성하는 방식은 다르지만, 지금까지 정리한 문법은 어디에서나 그대로 사용합니다.

각 문법을 더 깊이 이해하려면 아래 글을 참고하세요.