TypeScript 대 JavaScript: 이동 가이드
게시 됨: 2022-03-11TypeScript 또는 JavaScript? 개발자는 Greenfield 웹 또는 Node.js 프로젝트에 대해 이 선택을 고려하지만 기존 프로젝트에서도 고려할 가치가 있는 질문입니다. JavaScript의 상위 집합인 TypeScript는 JavaScript의 모든 기능과 몇 가지 추가 특전을 제공합니다. TypeScript는 본질적으로 우리가 깔끔하게 코드를 작성하도록 권장하여 코드를 더 확장 가능하게 만듭니다. 그러나 프로젝트에는 우리가 원하는 만큼의 일반 JavaScript가 포함될 수 있으므로 TypeScript를 사용하는 것은 '전부 아니면 전무'가 아닙니다.
TypeScript와 JavaScript의 관계
TypeScript는 JavaScript에 명시적 유형 시스템을 추가하여 변수 유형의 엄격한 적용을 허용합니다. TypeScript는 TypeScript 코드를 웹 브라우저와 Node.js가 이해하는 JavaScript 코드로 변환하는 컴파일 형식인 변환하는 동안 유형 검사를 실행합니다.
TypeScript 대 JavaScript 예제
유효한 JavaScript 스니펫부터 시작하겠습니다.
let var1 = "Hello"; var1 = 10; console.log(var1); 여기서 var1 은 string 으로 시작하여 number 가 됩니다.
JavaScript는 유형이 느슨하기 때문에 var1 을 문자열에서 함수에 이르기까지 모든 유형의 변수로 언제든지 재정의할 수 있습니다.
이 코드를 실행하면 10 이 출력됩니다.
이제 이 코드를 TypeScript로 변경해 보겠습니다.
let var1: string = "Hello"; var1 = 10; console.log(var1); 이 경우 var1 을 string 으로 선언합니다. 그런 다음 TypeScript의 엄격한 유형 시스템에서 허용하지 않는 숫자를 할당하려고 합니다. 트랜스파일하면 오류가 발생합니다.
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; 원본 JavaScript 스니펫을 TypeScript인 것처럼 처리하도록 트랜스파일러에 지시하면 트랜스파일러는 var1 이 string | number string | number . 이것은 TypeScript 공용체 유형 으로 var1 에 언제든지 string 이나 number 를 할당할 수 있습니다. 유형 충돌을 해결하면 TypeScript 코드가 성공적으로 변환됩니다. 실행하면 JavaScript 예제와 동일한 결과가 생성됩니다.
30,000피트에서 TypeScript 대 JavaScript: 확장성 문제
JavaScript는 1990년대 초기에는 상상할 수 없었던 방식으로 적용되어 모든 규모의 프로젝트를 지원하는 유비쿼터스입니다. JavaScript는 성숙했지만 확장성 지원에 관해서는 부족합니다. 따라서 개발자는 규모와 복잡성이 모두 증가한 JavaScript 응용 프로그램과 씨름합니다.
고맙게도 TypeScript는 JavaScript 프로젝트 확장과 관련된 많은 문제를 해결합니다. 검증, 리팩토링, 문서화라는 세 가지 주요 과제에 중점을 둘 것입니다.
확인
우리는 통합 개발 환경(IDE)에 의존하여 새 코드 추가, 수정 및 테스트와 같은 작업을 지원하지만 IDE는 순수한 JavaScript 참조의 유효성을 검사할 수 없습니다. 변수 및 함수 이름의 오타 가능성을 방지하기 위해 코드를 작성할 때 주의 깊게 모니터링하여 이 단점을 완화합니다.
코드가 타사에서 생성된 경우 문제의 규모가 기하급수적으로 증가합니다. 이 경우 거의 실행되지 않는 코드 분기의 깨진 참조가 쉽게 감지되지 않을 수 있습니다.
대조적으로 TypeScript를 사용하면 변환 시 오류가 식별될 것이라는 확신을 갖고 코딩에 집중할 수 있습니다. 이를 보여주기 위해 일부 레거시 JavaScript 코드로 시작하겠습니다.
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } .toISO() 호출은 moment.js toISOString() 메서드의 오타이지만 format 인수가 ISO 가 아닌 경우 코드가 작동합니다. ISO 를 함수에 처음 전달하려고 하면 다음 런타임 오류가 발생합니다. TypeError: moment(...).toISO is not a function .
철자가 틀린 코드를 찾기 어려울 수 있습니다. 현재 코드베이스에는 파선에 대한 경로가 없을 수 있습니다. 이 경우 깨진 .toISO() 참조는 테스트에서 포착되지 않습니다.
이 코드를 TypeScript로 이식하면 IDE에서 깨진 참조를 강조 표시하여 수정하라는 메시지를 표시합니다. 아무것도 하지 않고 트랜스파일을 시도하면 차단되고 트랜스파일러는 다음 오류를 생성합니다.
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());리팩토링
타사 코드 참조의 오타는 드문 일이 아니지만 다음과 같이 내부 참조의 오타와 관련된 다양한 문제가 있습니다.
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } 단독 개발자는 er 로 끝나는 모든 phoneNumbr 인스턴스를 찾아서 수정할 수 있습니다.
그러나 팀이 클수록 이 단순하고 일반적인 실수는 더 많은 비용이 듭니다. 업무를 수행하는 과정에서 동료들은 그러한 오타를 인지하고 전파해야 합니다. 또는 두 철자를 모두 지원하는 코드를 추가하면 코드베이스가 불필요하게 부풀려집니다.
TypeScript를 사용하면 오타를 수정하면 종속 코드가 더 이상 변환되지 않아 동료에게 수정 사항을 코드로 전파하라는 신호를 보냅니다.
선적 서류 비치
정확하고 관련성 있는 문서는 개발자 팀 내 및 팀 간의 의사 소통의 핵심입니다. JavaScript 개발자는 종종 JSDoc을 사용하여 예상되는 메서드 및 속성 유형을 문서화합니다.
TypeScript의 언어 기능(예: 추상 클래스, 인터페이스 및 유형 정의)은 계약에 따른 프로그래밍을 용이하게 하여 문서 품질을 향상시킵니다. 또한 개체가 준수해야 하는 메서드 및 속성에 대한 형식적인 정의가 있으면 주요 변경 사항을 식별하고, 테스트를 생성하고, 코드 자체 검사를 수행하고, 아키텍처 패턴을 구현하는 데 도움이 됩니다.
TypeScript의 경우 이동 도구 TypeDoc(TSDoc 제안 기반)는 코드에서 유형 정보(예: 클래스, 인터페이스, 메서드 및 속성)를 자동으로 추출합니다. 따라서 우리는 JSDoc보다 훨씬 더 포괄적인 문서를 손쉽게 만들 수 있습니다.
TypeScript와 JavaScript의 장점
이제 TypeScript를 사용하여 이러한 확장성 문제를 해결하는 방법을 살펴보겠습니다.
고급 코드/리팩토링 제안
많은 IDE가 TypeScript 유형 시스템의 정보를 처리하여 우리가 코딩할 때 참조 유효성 검사를 제공할 수 있습니다. 더군다나 우리가 입력할 때 IDE는 참조에 대한 관련 문서를 한 눈에 볼 수 있는 문서(예: 함수가 예상하는 인수)를 제공하고 상황에 따라 올바른 변수 이름을 제안할 수 있습니다.
이 TypeScript 스니펫에서 IDE는 함수의 반환 값 내에서 키 이름의 자동 완성을 제안합니다.
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }내 IDE인 Visual Studio Code는 함수를 호출하기 시작했을 때(31행) 다음 제안을 제공했습니다.
또한 IDE의 자동 완성 제안(설명선에 있음)은 문맥상 정확하며 중첩된 키 상황(34행) 내에서 유효한 이름만 표시합니다.
이러한 실시간 제안은 보다 빠른 코딩으로 이어집니다. 또한 IDE는 TypeScript의 엄격한 유형 정보에 의존하여 모든 규모의 코드를 리팩터링할 수 있습니다. 속성 이름 바꾸기, 파일 위치 변경 또는 슈퍼클래스 추출과 같은 작업은 참조의 정확성을 100% 확신할 때 사소한 일이 됩니다.
인터페이스 지원
JavaScript와 달리 TypeScript는 인터페이스 를 사용하여 유형을 정의하는 기능을 제공합니다. 인터페이스는 객체가 포함해야 하는 메서드와 속성을 공식적으로 나열하지만 구현하지는 않습니다. 이 언어 구성은 다른 개발자와의 공동 작업에 특히 유용합니다.
다음 예제에서는 TypeScript의 기능을 활용하여 일반적인 OOP 패턴(이 경우 전략 및 책임 사슬)을 깔끔하게 구현하여 이전 예제를 개선하는 방법을 강조합니다.

export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }ES6 모듈—어디서나
이 글을 쓰는 시점에서 모든 프론트엔드 및 백엔드 JavaScript 런타임이 ES6 모듈을 지원하는 것은 아닙니다. 그러나 TypeScript를 사용하면 ES6 모듈 구문을 사용할 수 있습니다.
import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); 트랜스파일된 출력은 선택한 환경과 호환됩니다. 예를 들어 컴파일러 옵션 --module CommonJS 를 사용하면 다음을 얻습니다.
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; 대신 --module UMD 를 사용하면 TypeScript가 더 자세한 UMD 패턴을 출력합니다.
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });ES6 클래스—어디서나
레거시 환경은 종종 ES6 클래스에 대한 지원이 부족합니다. TypeScript 변환은 대상별 구성을 사용하여 호환성을 보장합니다. 다음은 TypeScript 소스 스니펫입니다.
export class TestClass { hello = 'World'; }JavaScript 출력은 TypeScript에서 지정할 수 있는 모듈과 대상 모두에 따라 다릅니다.
--module CommonJS --target es3 의 결과는 다음과 같습니다.
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; 대신 --module CommonJS --target es6 사용하면 다음과 같은 트랜스파일 결과를 얻습니다. class 키워드는 ES6을 대상으로 하는 데 사용됩니다.
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;비동기/대기 기능—어디서나
Async/await를 사용하면 비동기 JavaScript 코드를 더 쉽게 이해하고 유지 관리할 수 있습니다. TypeScript는 기본적으로 비동기/대기를 제공하지 않는 런타임을 포함하여 모든 런타임에 이 기능을 제공합니다.
ES3 및 ES5와 같은 이전 런타임에서 async/await를 실행하려면 Promise 기반 출력에 대한 외부 지원이 필요합니다(예: Bluebird 또는 ES2015 폴리필을 통해). TypeScript와 함께 제공되는 Promise 폴리필은 변환된 출력에 쉽게 통합됩니다. 따라서 lib 컴파일러 옵션을 적절하게 구성하기만 하면 됩니다.
프라이빗 클래스 필드 지원—어디서나
레거시 대상의 경우에도 TypeScript는 강력한 형식의 언어(예: Java 또는 C#)와 거의 동일한 방식으로 private 필드를 지원합니다. 대조적으로, 많은 JavaScript 런타임은 ES2022의 완성된 제안인 해시 접두사 구문을 통해 private 필드를 지원합니다.
TypeScript와 JavaScript의 단점
TypeScript 구현의 주요 이점을 강조했으므로 이제 TypeScript가 적합하지 않을 수 있는 시나리오를 살펴보겠습니다.
번역: 워크플로 비호환성 가능성
특정 워크플로 또는 프로젝트 요구 사항은 TypeScript의 변환 단계와 호환되지 않을 수 있습니다. 예를 들어 배포 후 코드를 변경하기 위해 외부 도구를 사용해야 하거나 생성된 출력이 개발자 친화적이어야 하는 경우입니다.
예를 들어, 최근에 Node.js 환경을 위한 AWS Lambda 함수를 작성했습니다. 변환이 필요하면 나와 다른 팀원이 AWS 온라인 편집기를 사용하여 함수를 편집할 수 없기 때문에 TypeScript는 적합하지 않았습니다. 이것은 프로젝트 관리자의 거래 차단기였습니다.
유형 시스템은 변환 시간까지만 작동합니다.
TypeScript의 JavaScript 출력에는 유형 정보가 포함되어 있지 않으므로 유형 검사를 수행하지 않으므로 런타임에 유형 안전성이 손상될 수 있습니다. 예를 들어, 항상 객체를 반환하도록 함수가 정의되어 있다고 가정합니다. .js 파일 내에서 사용하여 null 이 반환되면 런타임 오류가 발생합니다.
유형 정보 종속 기능(예: 개인 필드, 인터페이스 또는 제네릭)은 모든 프로젝트에 가치를 추가하지만 트랜스파일하는 동안 스크랩됩니다. 예를 들어 private 클래스 멤버는 변환 후 더 이상 비공개가 아닙니다. 분명히 말해서, 이러한 특성의 런타임 문제는 TypeScript에만 있는 것이 아니며 JavaScript에서도 동일한 문제가 발생할 것으로 예상할 수 있습니다.
TypeScript와 JavaScript 결합
TypeScript의 많은 이점에도 불구하고 때로는 전체 JavaScript 프로젝트를 한 번에 변환하는 것을 정당화할 수 없습니다. 다행히 파일별로 TypeScript 변환기에 일반 JavaScript로 해석할 대상을 지정할 수 있습니다. 사실, 이 하이브리드 접근 방식은 프로젝트 수명 주기 동안 발생하는 개별 문제를 완화하는 데 도움이 될 수 있습니다.
코드가 다음과 같은 경우 JavaScript를 변경하지 않고 그대로 두는 것이 좋습니다.
- 이전 동료가 작성했으며 TypeScript로 변환하려면 상당한 리버스 엔지니어링 노력이 필요합니다.
- TypeScript에서 허용되지 않는 기술을 사용하며(예: 개체 인스턴스화 후 속성 추가) TypeScript 규칙을 준수하려면 리팩토링이 필요합니다.
- JavaScript를 계속 사용하는 다른 팀에 속합니다.
이러한 경우 선언 파일( .d.ts 파일, 정의 파일 또는 타이핑 파일이라고도 함)은 JavaScript 코드를 그대로 두면서 IDE 제안을 활성화하기에 충분한 유형 데이터를 TypeScript에 제공합니다.
많은 JavaScript 라이브러리(예: Lodash, Jest 및 React)는 별도의 유형 패키지에 TypeScript 타이핑 파일을 제공하는 반면 다른 라이브러리(예: Moment.js, Axios 및 Luxon)는 타이핑 파일을 기본 패키지에 통합합니다.
TypeScript 대 JavaScript: 간소화 및 확장성에 대한 질문
TypeScript를 통해 제공되는 타의 추종을 불허하는 지원, 유연성 및 개선 사항은 개발자 경험을 크게 개선하여 프로젝트와 팀을 확장할 수 있도록 합니다. TypeScript를 프로젝트에 통합하는 주요 비용은 변환 빌드 단계를 추가하는 것입니다. 대부분의 애플리케이션에서 JavaScript로 변환하는 것은 문제가 되지 않습니다. 오히려 TypeScript의 많은 이점을 위한 디딤돌입니다.
Toptal 엔지니어링 블로그에 대한 추가 정보:
- TypeScript 및 Jest 지원 작업: AWS SAM 자습서
