안녕하세요! 😊
타입스크립트를 공부하면서 헷갈렸던 부분들을 직접 정리해보았습니다.
개인적인 학습 기록이지만, 누군가에게 도움이 되셨으면 좋겠습니다! 🙇🏻♂️
현재 실무에서 TypeScript를 사용한 지 어느덧 2년을 바라보고 있습니다. 다양한 프로젝트를 경험하면서 동료 개발자들과의 협업도 많아졌고, 자연스럽게 '타입을 더 명확하고 유연하게 설계하는 방법은 없을까?'라는 고민이 생기기 시작했습니다.
간단하게 예를 들어, 아래와 같은 구조를 만들었던 적이 있습니다.
type Person = {
name: string;
age: number;
gender: "MALE" | "FEMALE";
address: string;
job: "developer" | "student" | "designer";
};
type PersonSummary = {
name: string;
age: number;
address: string;
};
당시에는 구조가 조금만 달라도 별도로 타입을 만들곤 했는데, 나중에 Omit 유틸리티 타입을 활용하면 더 간결하게 표현할 수 있다는 것을 알게 되었습니다.
type PersonSummary = Omit<Person, "gender" | "job">;
이처럼 타입스크립트에는 실무에서 더 유연하게 활용할 수 있는 기능들이 많다는 걸 새삼 느끼게 되었고, 이를 체계적으로 공부해보고 싶다는 생각이 들었습니다.
마침 한 입 챌린지 6기가 열리게 되어, 타입스크립트를 주제로 참여하게 되었습니다!
혼자 공부하면 느려질 때도 있고 지루해질 수 있는데, 이번 기회를 통해 집중력 있게 학습하고자 합니다.
이번 포스팅은 Day 1부터 Day 7까지, 챌린지 수업과 퀴즈를 따라가며 배운 내용과 인사이트를 정리해보려고 합니다!
자바스크립트를 더 안전하게 사용할 수 있도록 타입 관련 기능을 추가한 언어입니다.
자바스크립트의 유연함을 유지하면서, 타입 시스템을 도입해 더 안정적인 개발이 가능하도록 설계되었습니다.
모든 프로그래밍 언어는 "타입 시스템"을 갖고 있습니다.
타입 시스템의 차이에 따라 언어의 특징이 달라지게 됩니다.
각 언어는 타입 시스템을 가지고 있어 정적 타입 시스템이나 동적 타입 시스템을 갖게 되는데요!
대표적으로 정적 타입 시스템과 동적 타입 시스템이 있습니다.
그러면, 타입스크립트는 어떤 타입 시스템일까요?
타입스크립트는 정적 타임 시스템과 동적 타입 시스템의 장점을 모두 추구하는 독특한 시스템입니다.
저는 이전에 정적 타입 시스템이라고 생각했어요. 동적언어와 정적언어 둘중 하나만 고른다고 생각하면 정적 언어이기 때문이라고 생각했는데 독특한 시스템을 가지고 있다는 말에 뭔가 신기했습니다! 🧐
이 독특한 시스템인 이유는 다음과 같아요.
TypeScript는 우리가 작성한 코드를 내부적으로 AST(Abstract Syntax Tree, 추상 구문 트리)로 변환한 뒤, 해당 AST를 기반으로 타입 검사를 수행합니다. 이 과정에서 타입 오류가 발견되면 컴파일은 중단됩니다.
문제가 없다면 TypeScript는 코드를 JavaScript로 트랜스파일(transpile) 하며, 이후 실행은 일반 JavaScript와 동일하게 브라우저나 Node.js 런타임에서 AST → 바이트코드 → 실행 흐름을 따르게 됩니다.
즉, TypeScript 자체는 실행 환경이 아닌 정적 타입 검사기 + 트랜스파일러의 역할을 하며, 실행은 결국 JavaScript가 담당하게 되는 구조입니다.
둘째 날은 타입스크립트의 스코프와 기본 타입 이해하기의 주제에 가깝다고 생각 돼요.
다음과 같이 정리해봤습니다!
타입스크립트는 기본적으로 각 파일을 전역 스코프(global scope)로 인식합니다.
// index.ts
const a = 1;
// hello.ts
const a = 1;
위와 같이 작성하면, 두 파일 모두 전역 스코프에서 a라는 동일한 이름의 변수를 선언한 것으로 간주됩니다.
이로 인해 변수 중복 오류가 발생합니다.
그러면 전역 → 개별 모듈로 만들기 (모듈 스코프) 방법은?
타입스크립트에서 파일을 모듈(Module)로 인식시키려면 아래 방법을 사용합니다
1️⃣ export/import 사용하기
export
또는 import
구문이 존재하면, 해당 파일은 자동으로 모듈 스코프로 전환됩니다.2️⃣ moduleDetection 옵션 사용하기
tsconfig.json
파일에 moduleDetection
옵션을 설정하면, 타입스크립트가 보다 적극적으로 모듈을 감지합니다.{
"compilerOptions": {
"moduleDetection": "force"
}
}
이 설정을 사용하면 명시적인 export
없어도 모듈 스코프로 인식시킬 수 있습니다. (단, 프로젝트 설정에 따라 권장여부는 달라질 수 있습니다.)
타입 | 설명 | 예시 |
---|---|---|
number | 숫자 | let age: number = 30; |
string | 문자열 | let name: string = "John"; |
boolean | 참/거짓 | let isAdmin: boolean = true; |
null | 값 없음 | let data: null = null; |
undefined | 정의되지 않음 | let value: undefined = undefined; |
symbol | 고유하고 변경 불가능한 값 | let sym: symbol = Symbol("key"); |
bigint | 매우 큰 정수 | let big: bigint = 9007199254740991n; |
리터럴(Literal)은 값 그 자체 를 의미합니다
let a: 1; // a 변수는 오직 1이라는 값만 가질 수 있음
let b: "hello"; // b 변수는 오직 "hello"라는 문자열만 가질 수 있음
셋째 날은 객체 리터럴 타입과 다양한 객체 타입 패턴들에 대한 주제입니다.
TypeScript에서는 객체를 사용할 때, 객체의 속성(property)들과 그 타입을 명시적으로 선언하여 정적 타입 안정성을 확보할 수 있습니다.
아래는 객체 리터럴 타입을 직접 선언해서 객체를 정의한 예시입니다.
let user: {
id: number;
name: string;
} = {
id: 1,
name: "김범수",
};
참고:
C언어나 Java는 타입의 이름(name)을 기준으로 판단하는
명목적 타입 시스템(Nominal Type System)을 사용합니다.
절대 수정되어서는 안 되는 값에는 readonly를 붙여서 불변성을 보장할 수 있습니다.
let config: {
readonly apiKey: string;
} = {
apiKey: "MY_API_KEY",
};
config.apiKey = "HACKED"; // ❌ 오류 발생: 읽기 전용 속성입니다.
type User = {
id: number;
name: string;
};
let user: User = {
id: 1,
name: "이정환",
};
type CountryCodes = {
Korea: string;
UnitedStates: string;
};
let countryCodes = {
Korea: "ko",
UnitedStates: "us",
};
하지만 나라가 수십 개라면? 전부 작성하기 어렵습니다.
type CountryCodes = {
[key: string]: string;
};
let codes: CountryCodes = {
Korea: "ko",
UnitedStates: "us",
France: "fr",
};
type InvalidCodes = {
[key: string]: number;
Korea: string; // ❌ 오류! string은 number와 호환되지 않음
};
enum Direction {
Up,
Down,
Left,
Right,
}
let d: Direction = Direction.Up;
console.log(Direction.Up); // 0
console.log(Direction.Down); // 1
enum Status {
Success = 200,
NotFound = 404,
ServerError = 500,
}
Day 4는 any
, unknown
, void
, never
같은 특수 타입들을 중심으로 타입의 안전성과 계층 구조에 대해 다룬 날이었습니다! 🍀
특정 변수의 타입을 우리가 확실히 모를때 사용합니다. (치트키)
let value: any;
value = 1;
value = "hello";
value = true;
value = [1, 2, 3];
모든 값을 담을 수 있지만, 사용은 제한적
let value: unknown;
value = 1;
value = "hello";
// value + 1; ❌ 연산 불가능
if (typeof value === "number") {
console.log(value + 1); // ✅ 조건부 타입 검사 후 사용 가능
}
✅ 권장되는 타입: 모르는 값을 처리할 때는 any보다 unknown을 사용하세요.
"void -> 공허 -> 아무것도 없다."
"아무것도 반환하지 않음"을 의미하는 타입"
주로 return을 하지 않는 함수에 사용
undefined를 할당할 수 있고,
null도 가능하지만 strictNullChecks가 꺼져 있어야 함
존재하지 않는 불가능한 타입, 절대 발생할 수 없는 값
function error(): never {
throw new Error("에러 발생!");
}
function infinite(): never {
while (true) {}
}
undefined, null도 할당할 수 없음
any도 never에 할당할 수 없음
어떤 타입에도 할당 가능하지만, 반대로는 불가능
타입스크립트를 단순히 "타입을 붙이는 언어"라고 생각하면 한계가 있습니다.
정말로 이해하려면 아래 세 가지 기준을 중심으로 봐야 합니다.
어떤 기준으로 타입을 정의하는가?
어떤 기준으로 타입 간의 관계를 정의하는가?
어떤 기준으로 타입 오류를 검출하는가?
한 타입을 다른 타입으로 취급해도 괜찮은가?
number 리터럴 타입은 number타입에 호환된다
number type은 number 리터럴 타입에 호환되지 않는다.
예를 들면, number타입이 직사각형, number 리터럴 타입이 정사각형 이라고 생각하자!
let num1: number = 10;
let num2: 10 = 10;
num1 = num2; // ✅ 업캐스팅 (서브 → 슈퍼)
num2 = num1; // ❌ 다운캐스팅 (슈퍼 → 서브)
서브(sub)타입을 슈퍼(super)타입에 취급하는 것을 업캐스팅이라고 합니다.
반대로, 슈퍼(super)타입에 서브(sub)타입에 가면 다운캐스팅이라고 합니다.
타입계층도를 보면 이해가 잘 되는 것 같습니다!
📌 업캐스팅은 안전하지만, 다운캐스팅은 런타임 위험을 수반
업 캐스팅으로 unknown
에 할당을 할 순 있지만 다운캐스팅으로 서브(sub)에 할당을 할 수 없습니다!
never타입은 모든 타입에 서브타입이기때문에 어느 타입에든 할당이 가능.
function neverFunc() : never {
while(true) {}
}
let num: number = neverFunc() ok
let str: string = neverFunc() ok
let bool: boolean = neverFunc() ok
반대로 다운 캐스팅으로도 안된다. 어떠한 값도 저장할 수 없다.
any
는 타입 계층도 자체를 무시하는 특이한 존재입니다.
모든 타입에 슈퍼(super)타입에 위치하기도 하고 모든 타입에 서브(sub)타입이기도 합니다.
하지만, any
가 never
타입에 다운 캐스팅을 할 수 없습니다!!!
타입 | 특징 | 안전성 |
---|---|---|
any | 아무 값이나 할당 가능, 타입 검사 없음 | ❌ 매우 낮음 |
unknown | 아무 값이나 할당 가능, 사용은 제한됨 | ✅ 높음 |
void | 반환값 없음 | ✅ 적절한 용도에 사용 시 안전 |
never | 어떤 값도 할당 불가, 도달 불가능한 코드 | ✅ 타입 안전 보장 |
Day 5는 구조적 타입 호환성과 유니언/인터섹션 타입의 차이, 타입 추론이 중심이었습니다! 🔥
위에서 언급드렸지만 타입스크립트에서는 객체의 구조(프로퍼티) 를 기준으로 타입 간의 호환성을 판단합니다.
이를 구조적 타입 시스템(Structural Typing) 이라고 합니다.
// 슈퍼타입
type Animal = {
name: string;
color: string;
};
// 서브타입
type Dog = {
name: string;
color: string;
breed: string;
};
let animal: Animal = {
name: "기린",
color: "yellow",
};
let dog: Dog = {
name: "돌돌이",
color: "brown",
breed: "진도",
};
animal = dog; // ✅ OK: Dog는 Animal의 모든 속성을 포함하므로 업캐스팅
dog = animal; // ❌ Error: breed 속성이 없기 때문에 다운캐스팅은 불가
여러 타입 중 하나만 만족하면 되는 타입
type Developer = {
name: string;
language: string;
};
type Designer = {
name: string;
tool: string;
};
type Union1 = Developer | Designer;
let union1: Union1 = {
name: "이정환",
language: "TypeScript",
}; // ✅ Developer 만족
let union2: Union1 = {
name: "이정환",
tool: "Figma",
}; // ✅ Designer 만족
let union3: Union1 = {
name: "이정환",
language: "TypeScript",
tool: "Figma",
}; // ✅ 둘 다 만족해도 괜찮음
let union4: Union1 = {
name: "이정환",
}; // ❌ Error: Developer도, Designer도 모두 만족하지 못함
let variable: number & string;
type Dog = {
name: string;
color: string;
}
type Person = {
name: string;
language: string;
}
type Intersection = Dog & Person;
let intersection1: Intersection = {
name: "",
color: "",
language: ""
}
let intersection1: Intersection = {
name: "돌돌이",
color: "brown",
language: "Korean",
}; // ✅ 모든 속성 충족
타입스크립트는 대부분의 경우 타입을 자동으로 추론합니다.
변수 선언 시
let a = 10; // 🔍 a: number 로 추론됨
const b = 10; // 🔍 b: 10 (리터럴 타입) 으로 추론됨
let은 변수이므로 일반 타입(number 등) 으로 추론!
const는 재할당이 불가능하므로 리터럴 타입(고정값) 으로 추론!
대부분의 경우 명시적 타입 선언 없이도 정확하게 추론되므로
불필요하게 타입을 반복하지 않아도 됩니다.
개념 | 설명 | 특징 | |
---|---|---|---|
객체 타입 호환성 | 구조 기준으로 타입 호환 판단 | 프로퍼티가 더 많은 쪽이 더 적은 쪽으로 할당 가능 | |
유니언 타입 (` | `) | 여러 타입 중 하나만 만족하면 됨 | OR 조건 |
인터섹션 타입 (& ) | 여러 타입 모두 만족해야 함 | AND 조건 | |
타입 추론 | 타입을 명시하지 않아도 자동으로 추론됨 | const 는 리터럴, let 은 일반 타입 |
Day 6는 타입 단언(as, as const, !), 타입 좁히기, 그리고 서로소 유니온을 중심으로, 타입스크립트에서 ‘타입을 확정짓는 방법’에 대해 다루었습니다! 🔥
타입 단언이란, 개발자가 타입스크립트에게
“이 값은 내가 알고 있는 이 타입이 맞아!”라고 직접 말해주는 문법입니다.
const value = someValue as SomeType;
A가 B의 슈퍼 타입 이거나 A가 B의 서브타입이어야 합니다.
let num1 = 10 as never; // ✅ 가능 (number는 never의 슈퍼타입)
let num2 = 10 as unknown; // ✅ 가능 (unknown은 모든 타입의 슈퍼타입)
let num3 = 10 as string; // ❌ 에러 (number와 string은 호환되지 않음)
let num = 10 as const;
// num: 10 (number가 아니라 리터럴 타입)
let arr = [1, 2, 3] as const;
// arr: readonly [1, 2, 3]
옵셔널 체이닝(?.)에서 null 또는 undefined가 절대 아님을 보장하고 싶을 때 ! 사용합니다.
type Post = {
title: string;
author?: string;
};
let post: Post = {
title: "게시글 1",
author: "이정환",
};
const len: number = post.author!.length;
// `!` 덕분에 author는 절대 null이 아니라고 단언
function process(value: number | string | Date) {
if (typeof value === "number") {
console.log(value.toFixed()); // number로 좁혀짐
} else if (typeof value === "string") {
console.log(value.toUpperCase()); // string으로 좁혀짐
} else if (value instanceof Date) {
console.log(value.getFullYear()); // Date로 좁혀짐
}
}
교집합이 없는 유니온 타입을 구성하고, 공통된 식별자(tag)를 기준으로 조건 분기하는 패턴
type LoadingTask = {
state: "LOADING";
};
type FailedTask = {
state: "FAILED";
error: {
message: string;
};
};
type SuccessTask = {
state: "SUCCESS";
data: {
result: string;
};
};
type AsyncTask = LoadingTask | FailedTask | SuccessTask;
function processResult(task: AsyncTask) {
switch (task.state) {
case "LOADING":
console.log("로딩 중...");
break;
case "FAILED":
console.log("에러 발생:", task.error.message);
break;
case "SUCCESS":
console.log("성공:", task.data.result);
break;
}
}
const loading: AsyncTask = {
state: "LOADING",
};
const failed: AsyncTask = {
state: "FAILED",
error: {
message: "오류 발생 원인은 ~",
},
};
const success: AsyncTask = {
state: "SUCCESS",
data: {
result: "정상적으로 완료되었습니다",
},
};
개념 | 설명 |
---|---|
as 단언 | 값의 타입을 강제로 지정 |
as const | 값을 리터럴 타입 + readonly로 고정 |
! (non-null 단언) | null 또는 undefined가 아님을 단언 |
타입 좁히기 | 조건문 등을 활용해 타입을 구체화 |
서로소 유니온 | state 처럼 구분 가능한 값을 통해 타입 분기 처리 |
Day 7은 함수 타입을 어떻게 정의하고, 서로 다른 함수 타입 간에 호환 가능한지를 판단하는 기준에 대해 다루었습니다! 🔥
함수를 설명하는 가장 명확한 방법은 어떤 매개변수를 받고, 어떤 값을 반환하는지를 명시하는 것입니다.
// 함수를 설명하는 가장 좋은 방법
// js. 어떤 매개변수를 받고, 어떤 결과값을 반환하는지
// ts. 어떤 [타입의] 매개변수를 받고, 어떤 [타입의] 결과값을 반환하는지 이야기
function func(a: number, b: number): number {
return a + b;
}
type Add = (a: number, b: number) => number;
const add: Add = (a, b) => a + b;
type Operation = {
(a: number, b: number): number;
};
const add: Operation = (a, b) => a + b;
특정 함수 타입을 다른 함수 타입으로 취급해도 괜찮은가를 판단하는 기준!
1️⃣ 반환값의 타입이 호환되는가?
type A = () => number;
type B = () => 10;
let a: A = () => 10;
let b: B = () => 10;
a = b; // ✅ 업캐스팅 (리터럴 → 일반 타입)
b = a; // ❌ 다운캐스팅 (일반 → 리터럴)
리터럴 타입은 일반 타입에 할당 가능하지만, 그 반대는 다운캐스팅이므로 허용되지 않음
2️⃣ 매개변수의 타입이 호환되는가?
// 매개변수의 개수가 같을 때
type C = (value: number) => void;
type D = (value: 10) => void;
let c: C = (value) => {};
let d: D = (value) => {};
c = d; // ❌
d = c; // ✅
// 매개변수는 반대 방향입니다!
// 반환값: 서브 → 슈퍼 가능 (업캐스팅 가능)
// 매개변수: 슈퍼 → 서브 가능만 허용 (즉, 다운캐스팅만 허용)
type Animal = { name: string };
type Dog = { name: string; color: string };
let animalFunc = (animal: Animal) => {
console.log(animal.name);
};
let dogFunc = (dog: Dog) => {
console.log(dog.name);
console.log(dog.color);
};
animalFunc = dogFunc; // ❌
dogFunc = animalFunc; // ✅
매개변수가 많고 구체적일수록 더 좁은 타입입니다.
좁은 매개변수를 넓은 쪽으로 넣을 수는 있지만 그 반대는 위험합니다.
이렇게 된다고 가정하면
let testFunc = (animal: Animal) =>{
console.log(animal.name)
console.log(animal.color) // 말이 안되는 상황이 나오기 때문이다.
}
// 매개변수 갯수가 같을 때 함수타입의 호환성은 A(슈퍼) <- B(서브) 이땐 업캐스팅이 안된다
- 매개변수의 개수가 다를 때
type Func1 = (a: number, b: number) => void;
type Func2 = (a: number) => void;
let func1: Func1 = (a, b) => {};
let func2: Func2 = (a) => {};
func1 = func2; // ✅ 호출 시 두 번째 매개변수를 무시해도 되므로 가능
func2 = func1; // ❌ 호출 시 b 값이 없으면 에러
타입스크립트의 함수 타입 호환성은
실제 실행 환경에서 문제가 생기지 않도록 보장하기 위한 타입 설계 원칙입니다.
Day 1부터 Day 7까지의 학습을 정리하면서, 단순히 문법을 익히는 것을 넘어
타입이 어떻게 동작하고, 어떤 상황에서 어떤 타입을 선택해야 하는지에 대한 판단 기준이 생기기 시작했습니다.
개념을 글로 풀어내는 과정 자체가 복습이자 사고의 확장이었고,
실무에서 겪었던 모호함들이 차츰 명확해지는 느낌이 들었습니다.
정리하다 보니 분량이 상당히 길어져서,
나머지 Day 8부터 14까지의 내용은 다음 포스팅에서 이어서 공유드리겠습니다.
끝까지 읽어주셔서 감사합니다 🙇♂️
강의: 한 입 크기로 잘라먹는 Next.js(v15)
타입 계층도 이미지 출처: 한 입 크기로 잘라먹는 타입스크립트(TypeScript)