Backend
home
⚔️

JavaScript vs TypeScript — 플루타르크식 영웅 대결

생성일
2026/04/07 10:58
태그
JavaScript
TypeScript
플루타르크는 그리스와 로마의 영웅들을 나란히 세워 비교했다. 알렉산드로스와 카이사르, 아킬레우스와 헥토르. 둘 다 위대하지만, 어디서 갈리는지를 보면 각자의 본질이 드러난다.
JavaScript와 TypeScript도 그렇다. 둘 다 같은 전장에서 싸운다. 브라우저, Node.js, 프론트엔드, 백엔드. 그런데 싸우는 방식이 다르다. 하나는 자유롭고 빠르며, 하나는 단단하고 예측 가능하다.
어느 쪽이 옳다는 얘기가 아니다. 무엇이 다르고, 그 차이가 실제 코드에서 어떻게 드러나는지를 보는 게 목적이다.

1라운드 — 탄생 배경

JavaScript는 1995년에 열흘 만에 만들어졌다. 브렌던 아이크가 넷스케이프 브라우저에서 간단한 인터랙션을 처리하려고 만든 스크립트 언어였다. 처음부터 "빠르게, 유연하게"가 설계 원칙이었다. 타입 같은 건 나중 얘기였다.
TypeScript는 2012년 마이크로소프트가 공개했다. JavaScript로 대규모 애플리케이션을 짜다 보니 타입이 없어서 생기는 버그가 너무 많았다. 그래서 JavaScript 위에 정적 타입 시스템을 얹은 슈퍼셋을 만든 거다. TypeScript는 결국 JavaScript로 컴파일된다. 실행 환경은 동일하다.
JS는 전장에 뛰어든 전사다. TS는 갑옷을 입고 전략을 짜고 나온 전사다.

2라운드 — 타입 시스템

가장 근본적인 차이다. JavaScript는 동적 타입이고, TypeScript는 정적 타입이다.
JavaScript — 실행 전까지 타입을 모른다.
let value = 42; value = 'hello'; // 아무 문제 없다 value = true; // 이것도 된다 value = null; // 이것도
JavaScript
복사
타입이 언제든 바뀔 수 있다. 유연하지만 예측이 힘들다. 함수에 무엇을 넣든 JavaScript는 일단 실행한다.
function add(a, b) { return a + b; } add(1, 2); // 3 — 정상 add('1', 2); // '12' — 문자열 연결이 됐다 add([], {}); // '[object Object]' — 이게 뭔지도 모른다
JavaScript
복사
TypeScript — 컴파일 시점에 타입을 검사한다.
function add(a: number, b: number): number { return a + b; } add(1, 2); // 3 — 정상 add('1', 2); // 컴파일 에러 — 여기서 잡힌다
TypeScript
복사
잘못된 타입이 들어오면 코드가 실행되기도 전에 에러가 뜬다. 런타임에서 터지는 게 아니라 작성하는 순간 IDE가 빨간 줄을 그어준다.

3라운드 — 인터페이스와 타입 정의

JavaScript는 데이터 구조에 대한 명시적 계약이 없다.
// 이 객체가 어떤 구조인지 코드만 봐서는 알 수 없다 function renderUser(user) { return `${user.name} (${user.age})`; } // user.name이 있는지, age가 숫자인지 — 실행해봐야 안다
JavaScript
복사
TypeScript는 interfacetype으로 구조를 명시한다.
interface User { name: string; age: number; email?: string; // ? 는 선택적 프로퍼티 } function renderUser(user: User): string { return `${user.name} (${user.age})`; } // 이 함수는 User 구조를 반드시 받아야 한다 // 없는 프로퍼티 접근하면 컴파일 에러
TypeScript
복사
type은 좀 더 유연하다. 유니온 타입 같은 걸 만들 때 쓴다.
type Status = 'pending' | 'done' | 'failed'; type ID = string | number; let taskStatus: Status = 'pending'; taskStatus = 'done'; // OK taskStatus = 'unknown'; // 컴파일 에러 — Status에 없는 값
TypeScript
복사

4라운드 — 제네릭

JavaScript에는 제네릭이 없다. 대신 any처럼 뭐든 받아야 한다.
// JS — 타입 정보 없이 그냥 받는다 function identity(value) { return value; }
JavaScript
복사
TypeScript의 제네릭은 타입을 변수처럼 다룬다.
// T는 호출할 때 결정되는 타입 변수 function identity<T>(value: T): T { return value; } const str = identity<string>('hello'); // T = string const num = identity<number>(42); // T = number // 배열에서도 활용 function getFirst<T>(arr: T[]): T { return arr[0]; } getFirst([1, 2, 3]); // number 반환 getFirst(['a', 'b', 'c']); // string 반환
TypeScript
복사
재사용 가능하면서도 타입 안전한 함수를 만들 수 있다.

5라운드 — 클래스와 접근 제어자

JavaScript ES6부터 class 문법이 생겼다. 하지만 접근 제어는 여전히 약하다.
class BankAccount { constructor(owner, balance) { this.owner = owner; this.balance = balance; // 외부에서 바로 접근 가능 } deposit(amount) { this.balance += amount; } } const account = new BankAccount('Kim', 1000); account.balance = -99999; // 이게 그냥 된다 — 막을 방법이 없다
JavaScript
복사
TypeScript는 public, private, protected, readonly를 지원한다.
class BankAccount { readonly owner: string; private balance: number; constructor(owner: string, balance: number) { this.owner = owner; this.balance = balance; } deposit(amount: number): void { if (amount <= 0) throw new Error('금액이 올바르지 않다'); this.balance += amount; } getBalance(): number { return this.balance; } } const account = new BankAccount('Kim', 1000); account.balance = -99999; // 컴파일 에러 — private이라 접근 불가 account.owner = 'Lee'; // 컴파일 에러 — readonly라 변경 불가 account.deposit(500); // OK
TypeScript
복사

6라운드 — 실수를 잡는 시점

이게 실전에서 가장 체감되는 차이다.
JavaScript — 실수가 런타임에 터진다. 배포하고 나서, 사용자가 쓰다가 터진다.
const user = { name: 'Kim' }; console.log(user.addres.city); // TypeError: Cannot read properties of undefined // 실행해야만 안다
JavaScript
복사
TypeScript — 작성하는 순간 IDE가 알려준다.
interface User { name: string; address: { city: string }; } const user: User = { name: 'Kim', address: { city: 'Seoul' } }; console.log(user.addres.city); // 'addres' 속성이 없다 — 컴파일 에러, 오타를 바로 잡는다
TypeScript
복사

7라운드 — 진입 장벽과 생산성

JavaScript는 진입 장벽이 낮다. 파일 하나 만들고 브라우저에서 바로 실행된다. 타입 걱정 없이 일단 만들 수 있다.
TypeScript는 처음에 설정이 필요하다. tsconfig.json, 컴파일러, 타입 정의 파일(@types/*)까지. 초반에 느리다.
하지만 프로젝트 규모가 커질수록 역전된다. JS로 짠 대형 프로젝트는 나중에 손대기가 무섭다. 이 함수에 뭘 넣어야 하는지, 이 객체에 뭐가 있는지 — 코드를 전부 읽어야 안다. TS는 IDE가 다 알려준다.
단기전은 JS가 유리하다. 장기전은 TS가 유리하다.

8라운드 — 언제 뭘 써야 하나

둘 중 하나가 항상 옳다는 게 아니다. 상황에 따라 다르다.
JavaScript가 나은 경우
간단한 스크립트, 프로토타입, 빠른 POC
팀원이 TS에 익숙하지 않을 때
작은 프로젝트라 오버헤드가 부담될 때
TypeScript가 나은 경우
여러 사람이 함께 개발하는 프로젝트
장기간 유지보수해야 하는 코드베이스
API 응답이나 복잡한 데이터 구조를 다룰 때
큰 React / Next.js 프로젝트

결론 — 두 영웅의 본질

플루타르크는 영웅들을 비교하면서 어느 쪽이 낫다는 판결을 내리지 않았다. 각자의 덕목과 한계를 드러내는 것만으로 충분했다.
JavaScript는 자유롭고 즉각적이다. 빠르게 움직이는 데 최적화돼 있다. 하지만 그 자유가 때로는 예측 불가능한 결과를 낳는다.
TypeScript는 엄격하고 명시적이다. 코드의 의도가 타입으로 드러난다. 대신 그 엄격함이 초반엔 느리게 만든다.
둘은 경쟁 관계가 아니다. TypeScript는 JavaScript를 대체하지 않는다. TypeScript는 JavaScript가 된다. 컴파일하면 JS가 나온다. 브라우저는 여전히 JS만 이해한다.
JS를 잘 모르면 TS도 잘 쓸 수 없다. 순서가 있다.

실제 구현 코드 — Live Server로 바로 띄우기

아래 코드는 JS와 TS의 핵심 차이를 인터랙티브하게 비교해보는 페이지다. TypeScript는 브라우저에서 직접 실행되지 않으니, TS 코드는 "이렇게 쓴다"는 것을 보여주는 패널로 구성하고 실제 동작은 동일한 로직의 JS로 구현했다. index.html 하나로 Live Server 바로 실행 가능하다.
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>JS vs TS — 영웅 대결</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --js: #f7df1e; --js-dark: #c9b200; --ts: #3178c6; --ts-dark: #1a5ca8; --bg: #0d1117; --surface: #161b22; --surface2:#21262d; --border: #30363d; --text: #e6edf3; --sub: #8b949e; --green: #3fb950; --red: #f85149; --radius: 10px; } body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; } /* ── 헤더 ── */ header { background: var(--surface); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; } nav { max-width: 1000px; margin: 0 auto; padding: 0 20px; height: 56px; display: flex; justify-content: space-between; align-items: center; } .logo { font-weight: 800; font-size: 1.05rem; letter-spacing: -0.3px; } .logo .js-badge { color: var(--js); } .logo .ts-badge { color: var(--ts); } .tab-list { display: flex; gap: 4px; list-style: none; } .tab-list button { background: none; border: none; color: var(--sub); padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; transition: all 0.15s; } .tab-list button.active, .tab-list button:hover { background: var(--surface2); color: var(--text); } .tab-list button.active { font-weight: 700; } /* ── 레이아웃 ── */ main { max-width: 1000px; margin: 0 auto; padding: 36px 20px 60px; } .panel { display: none; } .panel.active { display: block; } .panel-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 6px; } .panel-desc { color: var(--sub); font-size: 0.9rem; margin-bottom: 28px; } /* ── 대결 카드 ── */ .vs-row { display: grid; grid-template-columns: 1fr auto 1fr; gap: 0; align-items: stretch; margin-bottom: 24px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } .vs-col { padding: 20px; background: var(--surface); } .vs-col.js-col { border-right: 1px solid var(--border); } .vs-col.ts-col { border-left: 1px solid var(--border); } .vs-divider { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 12px; background: var(--surface2); font-weight: 900; font-size: 0.9rem; color: var(--sub); min-width: 44px; } .lang-badge { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 700; margin-bottom: 10px; } .badge-js { background: var(--js); color: #000; } .badge-ts { background: var(--ts); color: #fff; } /* ── 코드 블록 ── */ pre { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; font-size: 0.82rem; line-height: 1.75; overflow-x: auto; font-family: 'Consolas', 'Fira Code', monospace; white-space: pre; } .kw { color: #ff7b72; } .fn { color: #d2a8ff; } .str { color: #a5d6ff; } .num { color: #79c0ff; } .cmt { color: #8b949e; font-style: italic; } .typ { color: #ffa657; } .ok { color: var(--green); font-weight: 700; } .err { color: var(--red); font-weight: 700; } /* ── 인터랙티브 박스 ── */ .demo-box { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; margin-bottom: 24px; } .demo-box h3 { font-size: 0.92rem; font-weight: 700; color: var(--sub); margin-bottom: 14px; text-transform: uppercase; letter-spacing: 0.5px; } .input-row { display: flex; gap: 10px; margin-bottom: 12px; } .input-row input, .input-row select { flex: 1; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; color: var(--text); padding: 9px 14px; font-size: 0.88rem; outline: none; } .input-row input:focus, .input-row select:focus { border-color: var(--ts); } .btn { padding: 9px 20px; border: none; border-radius: 8px; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: opacity 0.15s; } .btn:hover { opacity: 0.85; } .btn-js { background: var(--js); color: #000; } .btn-ts { background: var(--ts); color: #fff; } .btn-sm { padding: 5px 12px; font-size: 0.8rem; } .result-area { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; font-size: 0.88rem; min-height: 44px; font-family: monospace; line-height: 1.7; } /* ── 비교 표 ── */ .compare-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } .compare-table th, .compare-table td { padding: 12px 16px; border: 1px solid var(--border); text-align: left; } .compare-table th { background: var(--surface2); font-weight: 700; color: var(--sub); } .compare-table tr:nth-child(even) td { background: var(--surface); } .chip { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 0.75rem; font-weight: 700; } .chip-green { background: #3fb95020; color: var(--green); } .chip-red { background: #f8514920; color: var(--red); } .chip-gray { background: #8b949e20; color: var(--sub); } /* ── 타입 에러 시뮬레이터 ── */ .err-sim { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } @media (max-width: 600px) { .vs-row { grid-template-columns: 1fr; } .vs-divider { padding: 10px 0; } .err-sim { grid-template-columns: 1fr; } } .sim-card { background: var(--surface2); border-radius: 8px; padding: 16px; } .sim-card .sim-label { font-size: 0.75rem; font-weight: 700; margin-bottom: 10px; } .sim-card pre { font-size: 0.78rem; padding: 10px; } .sim-result { margin-top: 10px; font-size: 0.83rem; } .verdict-box { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin-top: 32px; } .verdict-box h3 { font-size: 1rem; font-weight: 700; margin-bottom: 14px; color: var(--js); } .verdict-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } @media (max-width: 500px) { .verdict-grid { grid-template-columns: 1fr; } } .verdict-col h4 { font-size: 0.85rem; font-weight: 700; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 2px solid; } .verdict-col.js h4 { border-color: var(--js); color: var(--js); } .verdict-col.ts h4 { border-color: var(--ts); color: var(--ts); } .verdict-col ul { list-style: none; display: flex; flex-direction: column; gap: 6px; } .verdict-col li { font-size: 0.85rem; padding: 6px 10px; background: var(--surface2); border-radius: 6px; color: var(--sub); } .verdict-col li::before { margin-right: 6px; } .verdict-col.js li::before { content: '⚡'; } .verdict-col.ts li::before { content: '🛡️'; } </style> </head> <body> <header> <nav> <span class="logo"> <span class="js-badge">JS</span> <span style="color:var(--sub)"> vs </span> <span class="ts-badge">TS</span> <span style="color:var(--sub); font-weight:400; font-size:0.9rem;"> — 영웅 대결</span> </span> <ul class="tab-list"> <li><button class="active" data-tab="tab-type">타입 시스템</button></li> <li><button data-tab="tab-func">함수 비교</button></li> <li><button data-tab="tab-class">클래스</button></li> <li><button data-tab="tab-sim">에러 시뮬레이터</button></li> <li><button data-tab="tab-summary">총정리</button></li> </ul> </nav> </header> <main> <!-- 탭 1: 타입 시스템 --> <div class="panel active" id="tab-type"> <p class="panel-title">⚔️ 1라운드 — 타입 시스템</p> <p class="panel-desc">JS는 실행 전까지 타입을 모른다. TS는 작성하는 순간 타입을 검사한다.</p> <div class="vs-row"> <div class="vs-col js-col"> <span class="lang-badge badge-js">JavaScript</span> <pre><span class="kw">let</span> value = <span class="num">42</span>; value = <span class="str">'hello'</span>; <span class="cmt">// OK — 타입 바뀜</span> value = <span class="kw">true</span>; <span class="cmt">// OK — 또 바뀜</span> value = <span class="kw">null</span>; <span class="cmt">// OK — 뭐든 된다</span></pre> </div> <div class="vs-divider">VS</div> <div class="vs-col ts-col"> <span class="lang-badge badge-ts">TypeScript</span> <pre><span class="kw">let</span> value: <span class="typ">number</span> = <span class="num">42</span>; value = <span class="str">'hello'</span>; <span class="err">// ✗ 에러</span> value = <span class="kw">true</span>; <span class="err">// ✗ 에러</span> value = <span class="num">100</span>; <span class="ok">// ✓ OK</span></pre> </div> </div> <div class="vs-row"> <div class="vs-col js-col"> <span class="lang-badge badge-js">JavaScript</span> <pre><span class="kw">function</span> <span class="fn">add</span>(a, b) { <span class="kw">return</span> a + b; } <span class="fn">add</span>(<span class="num">1</span>, <span class="num">2</span>); <span class="cmt">// 3</span> <span class="fn">add</span>(<span class="str">'1'</span>, <span class="num">2</span>); <span class="err">// '12' — 오작동</span> <span class="fn">add</span>([], {}); <span class="err">// '...object' — ??</span></pre> </div> <div class="vs-divider">VS</div> <div class="vs-col ts-col"> <span class="lang-badge badge-ts">TypeScript</span> <pre><span class="kw">function</span> <span class="fn">add</span>( a: <span class="typ">number</span>, b: <span class="typ">number</span> ): <span class="typ">number</span> { <span class="kw">return</span> a + b; } <span class="fn">add</span>(<span class="num">1</span>, <span class="num">2</span>); <span class="ok">// 3 ✓</span> <span class="fn">add</span>(<span class="str">'1'</span>, <span class="num">2</span>); <span class="err">// 컴파일 에러 ✗</span></pre> </div> </div> <!-- 인터랙티브 데모 --> <div class="demo-box"> <h3>🔬 직접 실험 — JS의 동적 타입 체험</h3> <div class="input-row"> <input type="text" id="addA" placeholder="첫 번째 값 (숫자 또는 문자)" /> <input type="text" id="addB" placeholder="두 번째 값" /> <button class="btn btn-js" id="runAdd">JS 실행</button> </div> <div class="result-area" id="addResult">값을 입력하고 실행해보자.</div> </div> </div> <!-- 탭 2: 함수 비교 --> <div class="panel" id="tab-func"> <p class="panel-title">⚔️ 2라운드 — 함수와 인터페이스</p> <p class="panel-desc">TS의 interface는 데이터 구조에 대한 명시적 계약이다.</p> <div class="vs-row"> <div class="vs-col js-col"> <span class="lang-badge badge-js">JavaScript</span> <pre><span class="cmt">// user에 뭐가 있는지 모른다</span> <span class="kw">function</span> <span class="fn">renderUser</span>(user) { <span class="kw">return</span> <span class="str">`${user.name} (${user.age})`</span>; } <span class="cmt">// 이게 맞는 구조인지 실행해야 안다</span> <span class="fn">renderUser</span>({ name: <span class="str">'Kim'</span> }); <span class="cmt">// → 'Kim (undefined)' — 오류지만 실행됨</span></pre> </div> <div class="vs-divider">VS</div> <div class="vs-col ts-col"> <span class="lang-badge badge-ts">TypeScript</span> <pre><span class="kw">interface</span> <span class="typ">User</span> { name: <span class="typ">string</span>; age: <span class="typ">number</span>; email?: <span class="typ">string</span>; <span class="cmt">// 선택적</span> } <span class="kw">function</span> <span class="fn">renderUser</span>(user: <span class="typ">User</span>): <span class="typ">string</span> { <span class="kw">return</span> <span class="str">`${user.name} (${user.age})`</span>; } <span class="fn">renderUser</span>({ name: <span class="str">'Kim'</span> }); <span class="err">// ✗ age 없음</span></pre> </div> </div> <div class="vs-row"> <div class="vs-col js-col"> <span class="lang-badge badge-js">JavaScript</span> <pre><span class="cmt">// 유니온 타입 — JS는 표현 불가</span> <span class="kw">function</span> <span class="fn">setStatus</span>(status) { <span class="cmt">// 'pending','done','failed' 외에도</span> <span class="cmt">// 뭐든 들어올 수 있다</span> console.<span class="fn">log</span>(status); }</pre> </div> <div class="vs-divider">VS</div> <div class="vs-col ts-col"> <span class="lang-badge badge-ts">TypeScript</span> <pre><span class="kw">type</span> <span class="typ">Status</span> = <span class="str">'pending'</span> | <span class="str">'done'</span> | <span class="str">'failed'</span>; <span class="kw">function</span> <span class="fn">setStatus</span>(s: <span class="typ">Status</span>): <span class="typ">void</span> { console.<span class="fn">log</span>(s); } <span class="fn">setStatus</span>(<span class="str">'done'</span>); <span class="ok">// ✓</span> <span class="fn">setStatus</span>(<span class="str">'unknown'</span>); <span class="err">// ✗ 에러</span></pre> </div> </div> <div class="demo-box"> <h3>🔬 User 객체 렌더링 시뮬레이터</h3> <div class="input-row"> <input type="text" id="userName" placeholder="이름 (name)" /> <input type="text" id="userAge" placeholder="나이 (age, 숫자)" /> <button class="btn btn-js" id="runUser">JS 방식</button> <button class="btn btn-ts" id="runUserTs">TS 방식 (검증)</button> </div> <div class="result-area" id="userResult">값을 입력하고 버튼을 눌러보자.</div> </div> </div> <!-- 탭 3: 클래스 --> <div class="panel" id="tab-class"> <p class="panel-title">⚔️ 3라운드 — 클래스와 접근 제어</p> <p class="panel-desc">JS 클래스는 외부에서 내부를 건드릴 수 있다. TS는 private으로 막는다.</p> <div class="vs-row"> <div class="vs-col js-col"> <span class="lang-badge badge-js">JavaScript</span> <pre><span class="kw">class</span> <span class="typ">BankAccount</span> { <span class="fn">constructor</span>(owner, balance) { <span class="kw">this</span>.owner = owner; <span class="kw">this</span>.balance = balance; } <span class="fn">deposit</span>(amount) { <span class="kw">this</span>.balance += amount; } } <span class="kw">const</span> acc = <span class="kw">new</span> <span class="typ">BankAccount</span>(<span class="str">'Kim'</span>, <span class="num">1000</span>); acc.balance = <span class="num">-99999</span>; <span class="err">// 이게 된다!</span></pre> </div> <div class="vs-divider">VS</div> <div class="vs-col ts-col"> <span class="lang-badge badge-ts">TypeScript</span> <pre><span class="kw">class</span> <span class="typ">BankAccount</span> { <span class="kw">readonly</span> owner: <span class="typ">string</span>; <span class="kw">private</span> balance: <span class="typ">number</span>; <span class="fn">constructor</span>(o: <span class="typ">string</span>, b: <span class="typ">number</span>) { <span class="kw">this</span>.owner = o; <span class="kw">this</span>.balance = b; } <span class="fn">deposit</span>(n: <span class="typ">number</span>): <span class="typ">void</span> { <span class="kw">if</span> (n <= <span class="num">0</span>) <span class="kw">throw</span> <span class="kw">new</span> Error(<span class="str">'오류'</span>); <span class="kw">this</span>.balance += n; } } <span class="kw">const</span> acc = <span class="kw">new</span> <span class="typ">BankAccount</span>(<span class="str">'Kim'</span>, <span class="num">1000</span>); acc.balance = <span class="num">-99999</span>; <span class="err">// ✗ 컴파일 에러</span></pre> </div> </div> <div class="demo-box"> <h3>🔬 은행 계좌 시뮬레이터 (JS 구현 + TS 규칙 적용)</h3> <div class="input-row"> <input type="text" id="bankOwner" placeholder="소유자 이름" /> <input type="text" id="bankBalance" placeholder="초기 잔액 (숫자)" /> <button class="btn btn-js" id="createAccount">계좌 생성</button> </div> <div class="input-row"> <input type="text" id="depositAmt" placeholder="입금 금액" /> <button class="btn btn-ts" id="doDeposit">입금</button> <input type="text" id="hackAmt" placeholder="임의 잔액 변경 시도" /> <button class="btn" style="background:var(--red);color:#fff" id="doHack">강제 변경 시도</button> </div> <div class="result-area" id="bankResult">계좌를 먼저 생성해보자.</div> </div> </div> <!-- 탭 4: 에러 시뮬레이터 --> <div class="panel" id="tab-sim"> <p class="panel-title">⚔️ 4라운드 — 에러가 터지는 시점</p> <p class="panel-desc">JS는 실행해봐야 안다. TS는 작성하는 순간 IDE가 알려준다.</p> <div class="err-sim" id="errorCases"></div> <div class="demo-box" style="margin-top:24px;"> <h3>🔬 typeof 체인 — JS의 방어 코드 vs TS의 타입 가드</h3> <div class="input-row"> <input type="text" id="guardInput" placeholder="값을 입력 (숫자, 문자 등)" /> <button class="btn btn-js" id="runGuardJs">JS typeof 체크</button> <button class="btn btn-ts" id="runGuardTs">TS 타입 가드</button> </div> <div class="result-area" id="guardResult">값을 입력하고 버튼을 눌러보자.</div> </div> </div> <!-- 탭 5: 총정리 --> <div class="panel" id="tab-summary"> <p class="panel-title">⚔️ 최종 판결 — 플루타르크식 총평</p> <p class="panel-desc">둘 다 위대하다. 하지만 쓰이는 전장이 다르다.</p> <table class="compare-table"> <thead> <tr> <th>항목</th> <th>JavaScript</th> <th>TypeScript</th> </tr> </thead> <tbody id="compareBody"></tbody> </table> <div class="verdict-box"> <h3>⚡ 플루타르크의 총평</h3> <div class="verdict-grid"> <div class="verdict-col js"> <h4>JavaScript — 아킬레우스</h4> <ul> <li>빠르고 자유롭다</li> <li>진입 장벽이 낮다</li> <li>브라우저에서 바로 실행</li> <li>프로토타입에 최적</li> <li>런타임 에러에 취약</li> </ul> </div> <div class="verdict-col ts"> <h4>TypeScript — 아이네이아스</h4> <ul> <li>엄격하고 예측 가능하다</li> <li>IDE 자동완성이 강력하다</li> <li>대형 프로젝트에 적합</li> <li>리팩토링이 안전하다</li> <li>JS로 컴파일되어 실행</li> </ul> </div> </div> </div> </div> </main> <script> // ── 탭 네비게이션 ── const tabBtns = document.querySelectorAll('.tab-list button'); const panels = document.querySelectorAll('.panel'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); panels.forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById(btn.dataset.tab).classList.add('active'); }); }); // ── 탭1: add 실험 ── document.getElementById('runAdd').addEventListener('click', () => { const rawA = document.getElementById('addA').value; const rawB = document.getElementById('addB').value; const a = isNaN(rawA) || rawA === '' ? rawA : Number(rawA); const b = isNaN(rawB) || rawB === '' ? rawB : Number(rawB); const result = a + b; const ta = typeof a, tb = typeof b, tr = typeof result; document.getElementById('addResult').innerHTML = `<span style="color:var(--sub)">a</span> = <span style="color:var(--js)">${JSON.stringify(a)}</span> <span style="color:var(--sub)">(${ta})</span>` + `&nbsp;+&nbsp;` + `<span style="color:var(--sub)">b</span> = <span style="color:var(--js)">${JSON.stringify(b)}</span> <span style="color:var(--sub)">(${tb})</span>` + `&nbsp;→&nbsp;` + `<span style="color:var(--green); font-weight:700;">${JSON.stringify(result)}</span> <span style="color:var(--sub)">(${tr})</span>` + (ta !== tb ? `<br><span style="color:var(--red); font-size:0.82rem;">⚠ 타입이 달라서 JS가 자동으로 형 변환했다. TS였다면 컴파일 에러.</span>` : ''); }); // ── 탭2: User 렌더러 ── document.getElementById('runUser').addEventListener('click', () => { const name = document.getElementById('userName').value; const age = document.getElementById('userAge').value; // JS 방식 — 그냥 붙인다 const result = `${name || 'undefined'} (${age || 'undefined'})`; document.getElementById('userResult').innerHTML = `<span style="color:var(--js)">[JS 방식]</span> → <strong>${result}</strong>` + (!age ? `<br><span style="color:var(--red); font-size:0.82rem;">⚠ age가 없어도 실행됨 — TS라면 컴파일 단계에서 막힌다.</span>` : ''); }); document.getElementById('runUserTs').addEventListener('click', () => { const name = document.getElementById('userName').value.trim(); const age = document.getElementById('userAge').value.trim(); const errors = []; if (!name) errors.push('name이 비어 있다 (string 필요)'); if (!age) errors.push('age가 비어 있다 (number 필요)'); if (age && isNaN(age)) errors.push(`age = "${age}" 는 number가 아니다`); if (errors.length) { document.getElementById('userResult').innerHTML = `<span style="color:var(--ts)">[TS 검증]</span> <span style="color:var(--red)">✗ 컴파일 에러</span><br>` + errors.map(e => `&nbsp;&nbsp;• ${e}`).join('<br>'); } else { document.getElementById('userResult').innerHTML = `<span style="color:var(--ts)">[TS 검증]</span> <span style="color:var(--green)">✓ 타입 통과</span> → <strong>${name} (${age})</strong>`; } }); // ── 탭3: 은행 계좌 ── let account = null; document.getElementById('createAccount').addEventListener('click', () => { const owner = document.getElementById('bankOwner').value.trim(); const balance = Number(document.getElementById('bankBalance').value); if (!owner || isNaN(balance)) { document.getElementById('bankResult').innerHTML = '<span style="color:var(--red)">이름과 초기 잔액을 올바르게 입력해야 한다.</span>'; return; } // TS의 private/readonly를 클로저로 모방 let _balance = balance; const _owner = owner; // readonly — 재할당 없음 account = { getOwner: () => _owner, getBalance: () => _balance, deposit: (n) => { if (typeof n !== 'number' || n <= 0) throw new Error('올바르지 않은 금액이다'); _balance += n; } }; render(); }); document.getElementById('doDeposit').addEventListener('click', () => { if (!account) return; const n = Number(document.getElementById('depositAmt').value); try { account.deposit(n); render(`입금 완료: +${n}`); } catch(e) { document.getElementById('bankResult').innerHTML += `<br><span style="color:var(--red)">✗ ${e.message}</span>`; } }); document.getElementById('doHack').addEventListener('click', () => { if (!account) return; const n = document.getElementById('hackAmt').value; // account._balance 는 클로저 밖에서 접근 불가 — TS private 시뮬레이션 document.getElementById('bankResult').innerHTML += `<br><span style="color:var(--red)">✗ [TS private 시뮬] 외부에서 balance에 직접 접근 불가. TS라면 컴파일 에러로 차단됨.</span>`; }); function render(msg) { if (!account) return; document.getElementById('bankResult').innerHTML = `소유자: <span style="color:var(--ts); font-weight:700">${account.getOwner()}</span>&nbsp;&nbsp;` + `잔액: <span style="color:var(--green); font-weight:700">${account.getBalance().toLocaleString()}원</span>` + (msg ? `<br><span style="color:var(--sub); font-size:0.83rem;">${msg}</span>` : ''); } // ── 탭4: 에러 케이스 카드 ── const errorCases = [ { title: 'undefined 프로퍼티 접근', js: `const user = { name: 'Kim' }; console.log(user.address.city); // → TypeError: Cannot read // properties of undefined // 실행해야 터진다`, ts: `const user = { name: 'Kim' }; console.log(user.address.city); // TS 에러: 'address' 속성이 // User 타입에 없다 // 작성 즉시 IDE가 알려준다`, jsTag: '런타임 에러', tsTag: '컴파일 에러' }, { title: '오타로 인한 속성 접근', js: `const user = { name: 'Kim', address: { city: 'Seoul' } }; user.addres.city; // → 런타임 에러 // 오타인지도 모른다`, ts: `interface User { name: string; address: { city: string }; } const user: User = { ... }; user.addres.city; // TS: 'addres' 없음. 혹시 // 'address'? 를 제안한다`, jsTag: '런타임 에러', tsTag: '컴파일 에러' }, { title: '함수 반환값 타입 불일치', js: `function getAge(user) { return user.age; // 뭐든 반환 } const age = getAge({ age: '스물' }); age * 2; // NaN — 조용히 실패`, ts: `function getAge( user: { age: number } ): number { return user.age; } getAge({ age: '스물' }); // TS: string은 number가 아님`, jsTag: '조용한 실패', tsTag: '컴파일 에러' }, { title: '잘못된 함수 인자 개수', js: `function greet(name, greeting) { return `${greeting}, ${name}!`; } greet('Kim'); // → 'undefined, Kim!' // 에러 없이 이상하게 실행됨`, ts: `function greet( name: string, greeting: string ): string { return ` + '`${greeting}, ${name}!`' + `; } greet('Kim'); // TS: 인수 1개, 매개변수 2개`, jsTag: '조용한 실패', tsTag: '컴파일 에러' }, ]; const grid = document.getElementById('errorCases'); errorCases.forEach(c => { const div = document.createElement('div'); div.className = 'sim-card'; div.innerHTML = ` <div class="sim-label" style="color:var(--sub)">${c.title}</div> <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;"> <div> <div style="font-size:0.72rem; font-weight:700; color:var(--js); margin-bottom:6px;">JavaScript</div> <pre>${c.js}</pre> <div class="sim-result"><span class="chip chip-red">${c.jsTag}</span></div> </div> <div> <div style="font-size:0.72rem; font-weight:700; color:var(--ts); margin-bottom:6px;">TypeScript</div> <pre>${c.ts}</pre> <div class="sim-result"><span class="chip chip-green">${c.tsTag}</span></div> </div> </div> `; grid.appendChild(div); }); // 타입 가드 데모 document.getElementById('runGuardJs').addEventListener('click', () => { const raw = document.getElementById('guardInput').value; let val; try { val = JSON.parse(raw); } catch { val = raw; } const t = typeof val; let msg = ''; if (t === 'number') msg = `숫자다. 2배 → ${val * 2}`; else if (t === 'string') msg = `문자열이다. 길이 → ${val.length}`; else if (t === 'boolean') msg = `불리언이다. 반전 → ${!val}`; else msg = `알 수 없는 타입: ${t}`; document.getElementById('guardResult').innerHTML = `<span style="color:var(--js)">[JS typeof]</span> typeof ${JSON.stringify(val)} = "${t}"<br>${msg}`; }); document.getElementById('runGuardTs').addEventListener('click', () => { const raw = document.getElementById('guardInput').value; let val; try { val = JSON.parse(raw); } catch { val = raw; } const t = typeof val; const allowed = ['number', 'string', 'boolean']; if (!allowed.includes(t)) { document.getElementById('guardResult').innerHTML = `<span style="color:var(--ts)">[TS 타입가드]</span> <span style="color:var(--red)">✗ "${t}" 타입은 허용되지 않는다. 컴파일 에러.</span>`; return; } document.getElementById('guardResult').innerHTML = `<span style="color:var(--ts)">[TS 타입가드]</span> <span style="color:var(--green)">✓ "${t}" 통과</span> — 이후 코드에서 ${t} 전용 메서드 사용 가능`; }); // ── 탭5: 비교 표 ── const compareRows = [ ['타입 방식', 'Dynamic (동적)', 'Static (정적)'], ['오류 발견 시점', '런타임 (실행 후)', '컴파일 타임 (작성 시)'], ['학습 곡선', '낮다', '높다 (초반)'], ['설정 필요', '불필요', 'tsconfig.json 등 필요'], ['IDE 지원', '기본 수준', '자동완성·추론 강력'], ['인터페이스', '없다 (JSDoc 사용)','interface / type'], ['접근 제어자', '없다', 'public / private / protected'], ['제네릭', '없다', '있다'], ['브라우저 실행', '직접 가능', 'JS로 컴파일 후 실행'], ['대형 프로젝트', '유지보수 어려움', '타입으로 구조 명확'], ['프로토타이핑', '빠르다', '초반 세팅 시간 필요'], ]; const tbody = document.getElementById('compareBody'); compareRows.forEach(([item, js, ts]) => { const tr = document.createElement('tr'); tr.innerHTML = ` <td style="font-weight:600; color:var(--sub)">${item}</td> <td style="color:var(--js)">${js}</td> <td style="color:#79b8f5">${ts}</td> `; tbody.appendChild(tr); }); </script> </body> </html>
HTML
복사