TypeScript로 견고한 코드 작성하기: 타입의 힘을 활용한 모던 개발 가이드
1. TypeScript란 무엇인가?
1.1 TypeScript의 탄생 배경과 역사
JavaScript는 웹 개발의 핵심 언어로 자리잡았지만, 대규모 애플리케이션 개발에서 여러 한계점을 보였습니다. 이러한 문제들을 해결하기 위해 Microsoft의 Anders Hejlsberg(C# 설계자)가 주도하여 2012년 TypeScript를 개발했습니다.
이러한 문제를 해결하기 위해 Microsoft는 2012년 TypeScript를 발표했습니다.
- 타입 시스템의 부재
- JavaScript는 동적 타입 언어이기 때문에 런타임에서만 타입 오류를 발견할 수 있었습니다
- 이는 버그 발견이 늦어지고, 코드 품질 관리가 어려워지는 원인이 되었습니다
- 대규모 개발의 어려움
- 코드베이스가 커질수록 유지보수가 어려워졌습니다
- 팀 단위 협업에서 코드 이해도와 생산성이 저하되었습니다
- 객체지향 프로그래밍 지원 부족
- JavaScript의 프로토타입 기반 상속은 복잡하고 직관적이지 않았습니다
- 클래스, 인터페이스 등 전통적인 OOP 개념의 부재로 구조적인 설계가 어려웠습니다
- IDE 지원의 한계
- 코드 자동완성, 리팩토링 등 개발 도구의 지원이 제한적이었습니다
- 정적 분석을 통한 개발자 생산성 향상이 어려웠습니다.
1.2 TypeScript vs JavaScript: 무엇이 다른가?
기본적인 차이점
// JavaScript
function calculateTotal(items) {
return items.reduce((total, item) => total + item.price, 0);
}
// 문제점: 런타임에서만 오류 발견
calculateTotal([
{name: 'item1'}, // price 속성이 없지만 코드 작성 시점에서는 오류를 발견할 수 없음
{price: 20}
]);
// TypeScript
interface Item {
name: string;
price: number;
}
function calculateTotal(items: Item[]): number {
return items.reduce((total, item) => total + item.price, 0);
}
// 컴파일 시점에서 오류 발견
calculateTotal([
{name: 'item1'}, // ❌ Error: price 속성이 없습니다
{price: 20} // ❌ Error: name 속성이 없습니다
]);
주요 차이점 설명
- 타입 시스템
- JavaScript: 동적 타입 - 런타임에 타입 확인
- TypeScript: 정적 타입 - 컴파일 시점에 타입 확인
- 개발 도구 지원
- JavaScript: 제한된 IDE 지원
- TypeScript: 풍부한 IDE 지원 (자동 완성, 리팩토링 등)
- 오류 감지
- JavaScript: 런타임에서만 오류 발견
- TypeScript: 컴파일 시점에서 대부분의 오류 발견
1.3 TypeScript의 동작 원리
TypeScript는 다음과 같은 과정을 거쳐 작동합니다:
- 컴파일 과정
- 시작: TypeScript 파일
- .ts 또는 .tsx 파일로 작성된 TypeScript 코드
- 구문 분석 (Parsing)
- TypeScript 코드를 파싱하여 AST(Abstract Syntax Tree) 생성
- 기본적인 구문 오류 검사
- 타입 검사 (Type Checking)
- 정적 타입 검사 수행
- 타입 오류 감지
- 변환 (Transformation)
- TypeScript 특정 문법을 JavaScript로 변환
- 타입 정보 제거
- 선택된 JavaScript 버전에 맞게 코드 변환
- 결과: JavaScript 파일
- 순수 JavaScript 코드 생성
- .js 파일 출력
tsconfig.json은 이 전체 컴파일 과정에 영향을 미치는 설정 파일입니다. 이 파일을 통해:
- 컴파일러 옵션 설정
- 출력 JavaScript 버전 지정
- 모듈 시스템 설정
- 타입 체크 엄격도 설정등을 제어할 수 있습니다.
- 타입 검사 시스템
// 타입 추론 예시
let message = "Hello"; // TypeScript가 자동으로 string 타입으로 추론
// 명시적 타입 지정
let message: string = "Hello"; // 개발자가 직접 타입을 지정
// 유니온 타입 예시
let id: string | number; // string 또는 number 타입 허용
id = "abc123"; // OK
id = 123; // OK
id = true; // ❌ Error: boolean 타입은 허용되지 않음
1.4 개발 환경 설정
TypeScript를 시작하기 위한 기본 환경 설정 방법을 알아보겠습니다:
Node.js 설치
# Node.js 설치 확인 node --version
TypeScript 설치
# 전역 설치 npm install -g typescript
프로젝트별 설치
# npm install typescript --save-dev
**TypeScript 설정 파일 (tsconfig.json)**
{
"compilerOptions": {
// JavaScript 출력 버전 설정
"target": "es5", // ES5 버전의 JavaScript로 컴파일
// 모듈 시스템 설정
"module": "commonjs", // Node.js에서 주로 사용하는 CommonJS 모듈 시스템 사용
// 타입 체크 관련 설정
"strict": true, // 모든 엄격한 타입-체킹 옵션 활성화
// 모듈 호환성 설정
"esModuleInterop": true, // CommonJS/AMD/UMD 모듈을 ES6 모듈처럼 사용 가능
// 성능 최적화 설정
"skipLibCheck": true, // 선언 파일(*.d.ts)의 타입 체크 스킵
// 파일 이름 일관성 설정
"forceConsistentCasingInFileNames": true // 파일 이름의 대소문자 일관성 강제
}
}
2. TypeScript 기본 타입과 문법
2.1 기본 타입 시스템
TypeScript의 기본 타입들을 실제 사용 사례와 함께 살펴보겠습니다.
2.1.1 원시 타입 (Primitive Types)
// 기본적인 타입 선언
const userName: string = "김철수";
const userAge: number = 30;
const isActive: boolean = true;
// undefined와 null
const notInitialized: undefined = undefined;
const empty: null = null;
// any와 unknown의 차이점
let anyValue: any = 4; // 어떤 타입이든 허용 (타입 검사를 하지 않음)
let unknownValue: unknown = 4; // 어떤 타입이든 허용 (타입 검사를 함)
// any 사용 예시 (권장하지 않음)
anyValue = "문자열"; // OK
anyValue = true; // OK
anyValue.toFixed(); // OK (컴파일 시점에서 오류를 발견하지 못함)
// unknown 사용 예시 (더 안전함)
unknownValue = "문자열"; // OK
unknownValue = true; // OK
// unknownValue.toFixed(); // ❌ Error: 타입 검사가 필요함
// 타입 검사 후 사용
if (typeof unknownValue === 'number') {
unknownValue.toFixed(); // OK
}
2.1.2 배열과 튜플
// 배열 타입 선언
const numbers: number[] = [1, 2, 3, 4, 5];
const names: Array<string> = ["철수", "영희", "민수"];
// 여러 타입을 가진 배열
const mixed: (string | number)[] = ["철수", 1, "영희", 2];
// 튜플 - 고정된 길이와 타입을 가진 배열
type UserInfo = [string, number, boolean];
const user: UserInfo = ["김철수", 30, true]; // 순서와 타입이 모두 일치해야 함
// 실제 사용 예시: React useState와 비슷한 구조
type State<T> = [T, (newValue: T) => void];
function useState<T>(initialValue: T): State<T> {
let value: T = initialValue;
const setValue = (newValue: T) => {
value = newValue;
};
return [value, setValue];
}
2.1.3 객체 타입
// 객체 타입 정의
interface User {
name: string;
age: number;
email?: string; // 선택적 속성 (optional property)
readonly id: number; // 읽기 전용 속성
}
// 객체 생성
const newUser: User = {
name: "김철수",
age: 30,
id: 1
};
// 인터페이스 확장
interface Employee extends User {
department: string;
role: string;
}
// 실제 사용 예시
const employee: Employee = {
name: "김영희",
age: 28,
id: 2,
department: "개발팀",
role: "프론트엔드 개발자"
};
// 타입 별칭(Type Alias)을 사용한 객체 타입
type Point = {
x: number;
y: number;
describe(): string;
};
const point: Point = {
x: 10,
y: 20,
describe() {
return `(${this.x}, ${this.y})`;
}
};
2.1.4 함수 타입
// 기본적인 함수 타입
function add(a: number, b: number): number {
return a + b;
}
// 화살표 함수와 타입
const multiply = (a: number, b: number): number => a * b;
// 함수 타입 정의
type MathOperation = (a: number, b: number) => number;
// 함수 타입 활용
const calculate = (
operation: MathOperation,
a: number,
b: number
): number => {
return operation(a, b);
};
// 오버로드된 함수 타입
function processValue(value: number): number;
function processValue(value: string): string;
function processValue(value: number | string): number | string {
if (typeof value === "number") {
return value * 2;
}
return value.toUpperCase();
}
2.2 타입 추론
TypeScript는 많은 경우 타입을 자동으로 추론할 수 있습니다:
// 변수 타입 추론
let message = "안녕하세요"; // 자동으로 string 타입으로 추론
let number = 42; // 자동으로 number 타입으로 추론
// 배열 타입 추론
let items = [1, 2, 3]; // number[] 타입으로 추론
let mixed = [1, "hello"]; // (string | number)[] 타입으로 추론
// 객체 타입 추론
const user = {
name: "김철수",
age: 30,
isAdmin: false
}; // { name: string; age: number; isAdmin: boolean; } 타입으로 추론
// 실제 사용 예시
const userList = [
{ id: 1, name: "김철수" },
{ id: 2, name: "이영희" }
]; // { id: number; name: string; }[] 타입으로 추론
// 함수 반환 타입 추론
function getFirstItem<T>(items: T[]) {
return items[0]; // 반환 타입이 T 또는 undefined로 추론됨
}
3. TypeScript 중급 문법과 고급 기능
3.1 제네릭(Generics)
제네릭이란?
제네릭은 타입을 마치 함수의 매개변수처럼 사용할 수 있게 해주는 기능입니다. 여러 타입에서 동작하는 컴포넌트를 만들 수 있으며, 타입 정보를 유지하면서 타입을 유연하게 처리할 수 있습니다.
any와의 차이점
// any를 사용할 경우
function getFirstAny(arr: any[]): any {
return arr[0];
}
const numberAny = getFirstAny([1, 2, 3]); // 타입: any
numberAny.toFixed(); // 컴파일러가 타입 체크를 못함
// 제네릭을 사용할 경우
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const number = getFirst([1, 2, 3]); // 타입: number
number.toFixed(); // 컴파일러가 타입 체크 가능
- 타입 안정성
- any: 모든 타입 검사를 비활성화
- 제네릭: 타입 안정성을 유지하면서 유연성 제공
- 타입 정보
- any: 타입 정보가 완전히 소실됨
- 제네릭: 타입 정보가 보존됨
- 컴파일러 지원
- any: IDE의 자동완성, 타입 체크 지원 불가
- 제네릭: IDE의 모든 타입 관련 기능 지원
제네릭이 필요한 이유
- 타입 안정성: 컴파일 시점에서 타입 체크가 가능
- 코드 재사용성: 다양한 타입에 대해 동일한 로직 적용 가능
- 타입 추론: 컴파일러가 타입을 자동으로 추론
실제 사용 사례
// 제네릭 타입 정의
interface Repository<T> {
findById: (id: number) => T;
save: (item: T) => void;
}
// 제네릭 함수
const createRepository = <T>(): Repository<T> => {
const items: T[] = [];
return {
findById: (id: number): T | undefined => {
return items.find(item => item.id === id);
},
save: (item: T): void => {
items.push(item);
}
};
};
//실제 사용
interface User {
id: number;
name: string;
}
// 유저 저장소 생성
const userRepository = createRepository<User>();
// 사용
userRepository.save({ id: 1, name: "Kim" });
const user = userRepository.findById(0); // { id: 1, name: "Kim" }
3.2 고급 타입
유니온 타입(Union Types)
두 개 이상의 타입을 하나의 타입으로 결합하는 방식입니다.
type StringOrNumber = string | number;
// 유니온 타입이 필요한 이유
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
인터섹션 타입(Intersection Types)
여러 타입을 하나로 결합하여 모든 타입의 기능을 가진 단일 타입을 만드는 방식입니다.
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface ArticleData {
title: string;
content: string;
}
type Article = ErrorHandling & ArticleData;
// 사용 예시
const article: Article = {
success: true,
title: "TypeScript 완벽 가이드",
content: "TypeScript는..."
};
리터럴 타입(Literal Types)
특정 문자열이나 숫자 값 자체를 타입으로 지정하는 방식입니다.
// 옷 사이즈는 "S", "M", "L" 중에서만 선택할 수 있음
type Size = "S" | "M" | "L";
// 신호등은 "빨간색", "노란색", "초록색" 중에서만 가능
type TrafficLight = "Red" | "Yellow" | "Green";
// 주사위는 1부터 6까지의 숫자만 가능
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
// 셔츠 주문 함수
function orderShirt(size: Size) {
console.log(`주문하신 ${size} 사이즈 셔츠를 준비하겠습니다.`);
}
orderShirt("M"); // 성공: "M"은 허용된 값
orderShirt("XL"); // 에러: "XL"은 Size 타입에 없음
- 유니온 타입: 여러 "타입들" 중 선택
- 리터럴 타입: 특정 "값들" 중 선택
타입 가드(Type Guards)
런타임에서 타입 검사를 수행하여 타입을 보장하는 방식입니다.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
// 타입 가드 함수
function isFish(pet: Fish | Bird): pet is Fish {
if ((pet as Fish).swim !== undefined) {
return true;
} else {
// pet is Bird
return false;
}
}
// 사용 예시
function moveAnimal(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // 이 스코프에서는 pet이 Fish 타입임을 보장
} else {
pet.fly(); // 이 스코프에서는 pet이 Bird 타입임을 보장
}
}
- isFish 의 반환 타입인 pet is Fish 는 술어 타입으로 타입 가드 역할을 하는 특별한 종류의 리턴 타입입니다.
컴파일러에게 함수의 반환값에 따라 pet의 타입을 좁힐 수 있다는 힌트를 제공하는 역할을 합니다.
이를 통해 타입 가드(Type Guard)를 구현할 수 있게 되는 거죠. 따라서 isFish 함수의 내부 로직에 의해 false가 반환될 수도 있습니다. 이 경우에는 pet이 Fish 타입이 아니라는 것을 의미하게 됩니다.
keyof 연산자
객체 타입을 받아서 그 객체의 모든 프로퍼티 키를 나열한 것과 같은 문자열 리터럴 유니온 타입을 생성합니다.
interface User {
id: number;
name: string;
email: string;
}
// keyof 사용
type UserKeys = keyof User; // "id" | "name" | "email"
// 실제 활용 예시
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = {
id: 1,
name: "John",
email: "john@example.com"
};
const userName = getProperty(user, "name"); // 타입 안전하게 접근 가능
3.3 고급 타입 시스템의 활용
조건부 타입 (Conditional Types)
TypeScript의 강력한 기능 중 하나인 조건부 타입을 살펴보겠습니다:
// 기본 조건부 타입 문법
type IsString<T> = T extends string ? true : false;
// 실제 사용 예시
type A = IsString<string>; // type A = true
type B = IsString<number>; // type B = false
// 실용적인 예제
type NonNullable<T> = T extends null | undefined ? never : T;
// 유틸리티 조건부 타입
type ExtractTypeFromArray<T> = T extends Array<infer U> ? U : never;
type NumberType = ExtractTypeFromArray<number[]>; // type NumberType = number
매핑된 타입 (Mapped Types)
기존 타입을 기반으로 새로운 타입을 생성하는 방법입니다:
// 기본 매핑된 타입
interface User {
name: string;
age: number;
email: string;
}
// 모든 필드를 선택적으로 만들기
type PartialUser = {
[K in keyof User]?: User[K];
};
// 모든 필드를 읽기 전용으로 만들기
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// 실제 활용 예시
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
const nullableUser: Nullable<User> = {
name: "김철수",
age: null,
email: "example@email.com"
};
4.실전 TypeScript 활용
4.1 React와 TypeScript
컴포넌트 타입 정의
// Props 인터페이스 정의
interface ButtonProps {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// 함수형 컴포넌트
const Button: React.FC<ButtonProps> = ({
text,
onClick,
variant = 'primary',
disabled = false
}) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
};
// 제네릭 컴포넌트
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
4.2 상태 관리와 TypeScript
Redux와 TypeScript
// 액션 타입 정의
enum ActionTypes {
ADD_TODO = 'ADD_TODO',
TOGGLE_TODO = 'TOGGLE_TODO'
}
// 액션 인터페이스
interface AddTodoAction {
type: ActionTypes.ADD_TODO;
payload: {
text: string;
};
}
interface ToggleTodoAction {
type: ActionTypes.TOGGLE_TODO;
payload: {
id: number;
};
}
type TodoActionTypes = AddTodoAction | ToggleTodoAction;
// 상태 인터페이스
interface TodoState {
todos: {
id: number;
text: string;
completed: boolean;
}[];
}
// 리듀서
const todoReducer = (
state: TodoState = initialState,
action: TodoActionTypes
): TodoState => {
switch (action.type) {
case ActionTypes.ADD_TODO:
return {
...state,
todos: [...state.todos, {
id: state.todos.length + 1,
text: action.payload.text,
completed: false
}]
};
case ActionTypes.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
};
5.성능 최적화와 모범 사례
5.1 타입 시스템 최적화
// ❌ 안티패턴: 과도한 any 사용
function processData(data: any) {
return data.someProperty; // 타입 안정성 없음
}
// ✅ 좋은 패턴: 제네릭 사용
function processData<T extends { someProperty: any }>(data: T) {
return data.someProperty;
}
// ❌ 안티패턴: 중복된 타입 정의
interface UserData {
name: string;
age: number;
}
interface AdminData {
name: string;
age: number;
role: string;
}
// ✅ 좋은 패턴: 타입 확장 사용
interface BaseUser {
name: string;
age: number;
}
interface Admin extends BaseUser {
role: string;
}
5.2 컴파일 최적화
{
"compilerOptions": {
"target": "es2020", // 컴파일된 코드의 대상 ECMAScript 버전을 지정합니다. 여기서는 ES2020을 대상으로 합니다.
"module": "esnext", // 컴파일된 코드에서 사용할 모듈 시스템을 지정합니다. 여기서는 ES 모듈을 사용합니다.
"strict": true, // 엄격한 타입 검사를 활성화합니다. 이는 더 나은 코드 품질과 안정성을 위해 권장됩니다.
"skipLibCheck": true, // 선언 파일의 유효성 검사를 건너뜁니다. 이는 컴파일 속도를 향상시킬 수 있습니다.
"forceConsistentCasingInFileNames": true, // 파일 이름의 대소문자 일관성을 강제합니다.
"esModuleInterop": true, // CommonJS 모듈을 ES 모듈처럼 가져올 수 있도록 설정합니다. 이는 모듈 간 상호 운용성을 향상시킵니다.
"moduleResolution": "node", // 모듈 해석 전략을 지정합니다. 여기서는 Node.js 스타일의 모듈 해석을 사용합니다.
"resolveJsonModule": true, // JSON 파일을 모듈로 가져올 수 있도록 설정합니다.
"isolatedModules": true, // 각 파일을 별도의 모듈로 컴파일하도록 설정합니다. 이는 더 나은 성능과 증분 컴파일을 가능하게 합니다.
"noEmit": true, // 컴파일 결과를 파일로 내보내지 않도록 설정합니다. 이는 타입 검사만 수행하고 실제 코드 생성은 하지 않습니다.
"jsx": "react-jsx" // JSX 코드 변환 방식을 지정합니다. 여기서는 React의 새로운 JSX 변환을 사용합니다.
}
}
6.디버깅과 테스팅
6.1 TypeScript 디버깅
// 타입 단언 사용
function assertIsString(val: any): asserts val is string {
if (typeof val !== "string") {
throw new Error("Not a string!");
}
}
// asserts는 TypeScript에서 함수가 어떤 조건을 만족하지 않으면 예외를 던질 것이라는 것을 나타내는 키워드입니다.
// 이를 "어서션 함수(Assertion Function)"라고 부릅니다.
// 디버깅을 위한 타입 가드
function isError(error: unknown): error is Error {
return error instanceof Error;
}
try {
// 어떤 작업 수행
} catch (error: unknown) {
if (isError(error)) {
console.error(error.message); // 타입 안전하게 에러 메시지 접근
}
}
//isError 함수는 error 매개변수가 Error 타입의 인스턴스인지 확인하는 타입 가드 함수입니다.
//try 블록에서 어떤 작업을 수행하고, 에러가 발생하면 catch 블록에서 처리합니다.
//catch 블록에서는 error 매개변수를 unknown 타입으로 받아, 어떤 타입의 에러든 처리할 수 있도록 합니다.
//isError 함수를 사용하여 error가 Error 타입인지 확인하고, 맞다면 error.message를 안전하게 사용할 수 있습니다.
7.마무리
TypeScript는 현대 웹 개발에서 필수적인 도구가 되었습니다. 이 가이드를 통해 기본 개념부터 고급 기능까지 다뤄보았습니다. 실제 프로젝트에서 TypeScript를 활용하면서 다음 사항들을 항상 고려하세요:
- 타입 안정성과 코드 품질 향상
- 개발자 경험 개선
- 생산성 향상
- 버그 조기 발견
TypeScript를 효과적으로 활용하면 더 안정적이고 유지보수가 용이한 애플리케이션을 개발할 수 있습니다.
이 글이 TypeScript를 시작하거나 깊이 있게 이해하는 데 도움이 되었기를 바랍니다. 추가 질문이나 의견이 있다면 댓글로 남겨주세요.
'TypeScript' 카테고리의 다른 글
[Typescript] - Exclude, Omit, Pick, Partial, Required, Record / 유틸리티 타입(Utility types) (0) | 2023.09.21 |
---|---|
typeScript (타입스크립트) 시작하기 (0) | 2022.04.18 |