Frontend
JavaScript vs. TypeScript: 무엇을 선택해야 할까?
JavaScript와 TypeScript의 핵심 차이를 타입 시스템, 개발 경험, 유지보수 관점에서 살펴보고 프로젝트에 적합한 선택 기준을 정리합니다.
웹 개발을 시작하면 거의 반드시 JavaScript를 만나게 됩니다. 그리고 프로젝트의 규모가 조금씩 커지기 시작하면 자연스럽게 TypeScript라는 이름도 자주 듣게 됩니다.
둘은 경쟁 관계처럼 보이지만, 사실 TypeScript는 JavaScript를 대체하기 위해 만들어진 완전히 다른 언어라기보다 JavaScript 위에 타입 시스템을 추가한 언어에 가깝습니다.
그렇다면 굳이 TypeScript를 사용해야 하는 이유는 무엇일까요? 반대로 JavaScript만 사용하는 것이 더 나은 상황도 있을까요?
이 글에서는 문법 차이를 나열하기보다, 실제 개발 과정에서 두 언어가 어떤 차이를 만드는지 중심으로 정리해보겠습니다.
JavaScript와 TypeScript의 관계
먼저 가장 중요한 부분부터 짚어보겠습니다.
JavaScript는 브라우저와 Node.js 같은 JavaScript Runtime에서 직접 실행되는 동적 타입(dynamic typing) 언어입니다.
반면 TypeScript는 Microsoft가 개발한 언어로, JavaScript 문법을 기반으로 정적 타입 시스템(static type system)을 추가합니다.
간단하게 표현하면 다음과 같습니다.
JavaScript + Static Type System ≈ TypeScript
TypeScript는 JavaScript의 superset이기 때문에 대부분의 JavaScript 코드는 TypeScript 코드로도 사용할 수 있습니다.
예를 들어 아래 코드는 정상적인 JavaScript입니다.
function add(a, b) {
return a + b;
}
add(10, 20);
동시에 TypeScript에서도 사용할 수 있습니다.
하지만 TypeScript에서는 타입을 명시할 수도 있습니다.
function add(a: number, b: number): number {
return a + b;
}
add(10, 20);
차이는 코드가 실행될 때보다 코드를 작성하는 과정에서 나타납니다.
가장 큰 차이: 타입을 언제 확인하는가
JavaScript에서는 변수의 타입이 런타임에 결정됩니다.
let count = 10;
count = "ten";
JavaScript 입장에서 이 코드는 문법적으로 문제가 없습니다.
하지만 프로그램의 다른 부분에서 count가 계속 숫자라고 가정하고 있다면 문제가 발생할 수 있습니다.
function double(value) {
return value * 2;
}
double(count);
이러한 문제는 실제 코드가 실행된 이후에야 발견되는 경우가 많습니다.
TypeScript에서는 상황이 조금 다릅니다.
let count: number = 10;
count = "ten";
TypeScript compiler는 "ten"을 number 변수에 할당하려는 순간 오류를 알려줍니다.
즉,
JavaScript
코드 작성 → 실행 → 오류 발견
TypeScript
코드 작성 → 타입 검사 → 오류 발견 → 실행
이라는 차이가 생깁니다.
물론 TypeScript가 모든 런타임 오류를 방지하는 것은 아닙니다. API 장애, 잘못된 외부 데이터, 네트워크 오류처럼 타입 시스템만으로 해결할 수 없는 문제는 여전히 존재합니다.
TypeScript가 제공하는 핵심적인 가치는 실행하기 전에 발견할 수 있는 오류의 범위를 넓혀주는 것이라고 보는 편이 정확합니다.
객체가 복잡해질수록 차이가 커진다
두 언어의 차이는 작은 코드에서는 크게 느껴지지 않을 수 있습니다.
예를 들어 다음 JavaScript 함수는 충분히 이해하기 쉽습니다.
function printUser(user) {
console.log(user.name);
}
하지만 여기서 user가 어떤 객체인지 코드만 보고 바로 알기는 어렵습니다.
printUser({
name: "Kim",
age: 30,
});
프로젝트가 커지면 질문이 늘어납니다.
name은 항상 존재할까요?
age는 number일까요, string일까요?
email은 optional일까요?
TypeScript에서는 이러한 계약(contract)을 코드에 표현할 수 있습니다.
interface User {
name: string;
age: number;
email?: string;
}
function printUser(user: User) {
console.log(user.name);
}
이제 IDE와 compiler가 User의 구조를 알고 있습니다.
잘못된 데이터를 전달하면 바로 확인할 수 있습니다.
printUser({
name: "Kim",
age: "30",
});
age가 number여야 한다는 사실을 TypeScript가 알려줍니다.
이 차이는 특히 API response, component props, domain model처럼 객체 구조가 반복적으로 사용되는 프로젝트에서 크게 체감됩니다.
TypeScript의 장점은 오류 검사만이 아니다
처음 TypeScript를 접하면 보통 "타입 오류를 잡아주는 JavaScript" 정도로 이해하기 쉽습니다.
하지만 실제 프로젝트에서는 개발 도구의 품질도 상당히 중요한 장점입니다.
예를 들어 다음과 같은 타입이 있다고 해보겠습니다.
interface User {
id: number;
name: string;
email: string;
}
const user: User = {
id: 1,
name: "Lee",
email: "[email protected]",
};
IDE는 user.을 입력하는 순간 가능한 property를 알고 있습니다.
user.id
user.name
user.email
자동 완성뿐 아니라 함수 parameter, return type, property 관계도 추적할 수 있기 때문에 코드 탐색과 refactoring도 훨씬 안정적입니다.
특히 여러 사람이 같은 코드를 수정하는 프로젝트에서는 타입 자체가 일종의 실행 가능한 문서(documentation) 역할을 하기도 합니다.
JavaScript가 더 단순한 부분도 있다
그렇다고 TypeScript가 언제나 더 좋은 선택인 것은 아닙니다.
JavaScript는 별도의 타입 정의 없이 바로 코드를 작성하고 실행할 수 있습니다.
const greeting = (name) => {
return `Hello, ${name}`;
};
TypeScript에서는 프로젝트 환경에 따라 compiler 설정이나 build tool 설정이 추가됩니다.
대표적으로 tsconfig.json을 관리해야 합니다.
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext"
}
}
라이브러리를 사용할 때 타입 선언이 필요할 수도 있고, 복잡한 generic이나 union type을 다루다 보면 오히려 타입을 설계하는 데 상당한 시간이 들어갈 수도 있습니다.
즉 TypeScript에는 분명 비용이 있습니다.
TypeScript의 이점
+ compile-time type checking
+ IDE support
+ safer refactoring
+ explicit contracts
+ large-scale maintainability
TypeScript의 비용
- type definitions
- compiler configuration
- additional learning curve
- occasionally complex type errors
중요한 것은 "TypeScript가 더 좋은 언어인가?"보다는 **이 비용을 지불할 만큼 프로젝트가 복잡한가?**에 가깝습니다.
JavaScript와 TypeScript 비교
| 항목 | JavaScript | TypeScript |
|---|---|---|
| Typing | Dynamic | Static + Dynamic |
| 실행 | Runtime에서 직접 실행 | JavaScript로 변환 후 실행 |
| Type checking | Runtime 중심 | Compile time 중심 |
| 초기 설정 | 간단함 | 상대적으로 필요함 |
| IDE 지원 | 좋음 | 일반적으로 더 강력함 |
| Refactoring | 규모가 커지면 위험 증가 | 타입 정보를 활용 가능 |
| 학습 난이도 | 상대적으로 낮음 | 타입 시스템 학습 필요 |
| 작은 프로젝트 | 매우 적합 | 경우에 따라 과할 수 있음 |
| 대규모 프로젝트 | 관리 규칙이 중요 | 일반적으로 유리함 |
여기서 한 가지 주의할 부분이 있습니다.
TypeScript를 사용한다고 JavaScript를 몰라도 되는 것은 아닙니다.
실제로 브라우저나 Node.js가 실행하는 것은 결국 JavaScript이고, TypeScript의 runtime behavior 역시 JavaScript의 동작을 따릅니다.
Promise, closure, prototype, event loop, module system 같은 JavaScript 개념은 TypeScript에서도 그대로 중요합니다.
TypeScript도 결국 JavaScript가 된다
TypeScript 코드가 브라우저에서 그대로 실행된다고 생각하기 쉽지만 일반적인 환경에서는 그렇지 않습니다.
다음 TypeScript 코드가 있다고 해보겠습니다.
const message: string = "Hello TypeScript";
console.log(message);
컴파일 과정에서는 타입 정보가 제거됩니다.
결과 JavaScript는 개념적으로 다음과 비슷합니다.
const message = "Hello TypeScript";
console.log(message);
string이라는 타입 정보는 runtime에는 존재하지 않습니다.
따라서 TypeScript의 타입 시스템은 기본적으로 개발 및 컴파일 단계에서 개발자를 돕는 도구입니다.
이 점을 이해하면 TypeScript의 역할이 훨씬 명확해집니다.
any를 많이 사용하면 TypeScript의 장점이 줄어든다
TypeScript를 도입했다고 자동으로 코드가 안전해지는 것은 아닙니다.
대표적인 예가 any입니다.
function getUser(data: any) {
return data.user.profile.name;
}
any를 사용하면 TypeScript는 해당 값에 대한 타입 검사를 대부분 포기합니다.
그래서 가능하면 구체적인 타입을 표현하는 것이 좋습니다.
interface Profile {
name: string;
}
interface User {
profile: Profile;
}
interface ResponseData {
user: User;
}
function getUser(data: ResponseData) {
return data.user.profile.name;
}
물론 실제 프로젝트에서는 모든 타입을 이렇게 세분화하는 것도 유지보수 비용이 될 수 있습니다.
결국 좋은 TypeScript 코드는 타입을 최대한 많이 작성하는 코드가 아니라 필요한 경계를 명확하게 표현하는 코드에 가깝습니다.
외부 데이터는 TypeScript만 믿으면 안 된다
TypeScript를 사용할 때 자주 생기는 오해가 하나 있습니다.
API response에 타입을 지정하면 실제 서버 응답도 그 타입을 만족한다고 생각하는 것입니다.
interface User {
id: number;
name: string;
}
const response = await fetch("/api/user");
const user: User = await response.json();
코드에서는 user가 User라고 선언되어 있지만 서버가 실제로 다음 데이터를 반환할 수도 있습니다.
{
"id": "wrong-value",
"username": "Kim"
}
TypeScript의 타입은 runtime에서 이 데이터를 검사하지 않습니다.
따라서 신뢰할 수 없는 외부 입력을 다룰 때는 schema validation 같은 runtime validation이 별도로 필요할 수 있습니다.
External Data
↓
Runtime Validation
↓
Validated Type
↓
Application Logic
이 구분은 TypeScript 프로젝트를 운영할수록 중요해집니다.
그렇다면 JavaScript는 언제 선택할까?
JavaScript는 여전히 훌륭한 선택입니다.
특히 다음과 같은 경우에는 JavaScript의 단순함이 장점이 될 수 있습니다.
- 작은 prototype
- 간단한 automation script
- 빠르게 검증해야 하는 아이디어
- 학습 목적의 작은 프로젝트
- 타입 시스템의 유지 비용보다 코드 규모가 훨씬 작은 경우
예를 들어 30줄짜리 간단한 script에 복잡한 generic type을 설계하는 것은 얻는 것보다 비용이 클 수 있습니다.
JavaScript는 빠르게 작성하고 바로 실행할 수 있다는 점에서 여전히 강력합니다.
TypeScript는 언제 선택할까?
반대로 다음과 같은 프로젝트라면 TypeScript의 장점이 커집니다.
- 여러 개발자가 함께 관리하는 프로젝트
- 장기간 유지보수해야 하는 서비스
- domain model이 복잡한 애플리케이션
- API와 데이터 구조가 많은 프로젝트
- React component의 props가 복잡한 프로젝트
- refactoring이 자주 발생하는 codebase
- frontend와 backend 사이의 타입 계약이 중요한 프로젝트
특히 프로젝트 규모가 커질수록 코드 한 줄을 작성하는 속도보다 기존 코드를 안전하게 변경하는 능력이 중요해집니다.
이 지점에서 TypeScript의 가치가 크게 나타납니다.
JavaScript에서 TypeScript로 이동하는 방법
기존 JavaScript 프로젝트가 있다고 해서 모든 파일을 한 번에 TypeScript로 변경할 필요는 없습니다.
TypeScript는 점진적으로 도입할 수 있습니다.
예를 들어,
JavaScript Project
↓
TypeScript configuration 추가
↓
새로운 코드부터 TypeScript 사용
↓
중요한 module부터 migration
↓
점진적으로 type coverage 확대
와 같은 방식으로 접근할 수 있습니다.
실제 서비스에서는 "완벽한 TypeScript 전환" 자체를 목표로 하기보다 오류 가능성이 높거나 변경이 잦은 영역부터 타입을 추가하는 전략이 현실적일 때가 많습니다.
결국 무엇을 선택해야 할까?
JavaScript와 TypeScript 중 하나가 절대적으로 우월하다고 보기는 어렵습니다.
두 언어의 관계를 다음처럼 생각하면 이해하기 쉽습니다.
JavaScript는 실행되는 언어이고, TypeScript는 JavaScript를 더 안전하게 작성하도록 도와주는 타입 시스템과 개발 도구를 제공한다.
개인적인 작은 script나 빠른 prototype이라면 JavaScript의 단순함이 충분히 매력적입니다.
반면 여러 사람이 오랫동안 관리해야 하는 서비스라면 TypeScript가 제공하는 타입 검사, 자동 완성, 코드 탐색, refactoring 안정성이 점점 중요해집니다.
그리고 TypeScript를 선택하더라도 가장 중요한 기반은 여전히 JavaScript입니다.
좋은 TypeScript 개발자가 되기 위해서는 타입 문법만 배우는 것보다 JavaScript가 runtime에서 어떻게 동작하는지 이해하는 것이 먼저입니다.
결국 선택 기준은 언어 자체보다는 프로젝트의 규모, 수명, 복잡도 그리고 협업 방식에 있습니다.
Tags