paint-brush
TypeScript를 진정한 "강력한 형식"으로 만들기by@nodge
15,774
15,774

TypeScript를 진정한 "강력한 형식"으로 만들기

Maksim Zemskov12m2023/09/10
Read on Terminal Reader
Read this story w/o Javascript

TypeScript는 데이터의 형태를 미리 알 수 없는 상황을 위해 "Any" 유형을 제공합니다. 그러나 이 유형을 과도하게 사용하면 유형 안전성, 코드 품질 및 개발자 경험에 문제가 발생할 수 있습니다. 이 문서에서는 "Any" 유형과 관련된 위험을 살펴보고, 해당 유형이 코드베이스에 포함될 수 있는 잠재적인 소스를 식별하고, 프로젝트 전체에서 해당 유형의 사용을 제어하기 위한 전략을 제공합니다.

People Mentioned

Mention Thumbnail
featured image - TypeScript를 진정한 "강력한 형식"으로 만들기
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript는 JavaScript를 기반으로 구축된 강력한 형식의 프로그래밍 언어로 어떤 규모에서든 더 나은 도구를 제공한다고 주장합니다. 그러나 TypeScript에는 암시적으로 코드베이스에 몰래 들어가 TypeScript의 많은 장점을 잃을 수 있는 any 유형이 포함되어 있습니다.


이 문서에서는 TypeScript 프로젝트의 any 유형을 제어하는 방법을 살펴봅니다. TypeScript의 강력한 기능을 활용하여 최고의 유형 안전성을 달성하고 코드 품질을 향상할 준비를 하세요.

TypeScript에서 Any를 사용할 때의 단점

TypeScript는 개발자 경험과 생산성을 향상시키기 위한 다양한 추가 도구를 제공합니다.


  • 개발 단계 초기에 오류를 잡는 데 도움이 됩니다.
  • 코드 편집기 및 IDE에 탁월한 자동 완성 기능을 제공합니다.
  • 환상적인 코드 탐색 도구와 자동 리팩토링을 통해 대규모 코드베이스를 쉽게 리팩토링할 수 있습니다.
  • 유형을 통해 추가 의미 체계와 명시적 데이터 구조를 제공하여 코드베이스에 대한 이해를 단순화합니다.


그러나 코드베이스에서 any 유형을 사용하기 시작하자마자 위에 나열된 모든 이점을 잃게 됩니다. any 유형은 유형 시스템의 위험한 허점이며 이를 사용하면 모든 유형 검사 기능은 물론 유형 검사에 의존하는 모든 도구도 비활성화됩니다. 결과적으로 TypeScript의 모든 이점이 사라집니다. 버그가 누락되고 코드 편집기의 유용성이 떨어지는 등의 일이 발생합니다.


예를 들어 다음 예를 고려해보세요.


 function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any


위의 코드에서:


  • parse 기능 내에서 자동 완성을 놓칠 수 있습니다. data. 편집기에서는 data 에 사용 가능한 방법에 대한 올바른 제안이 제공되지 않습니다.
  • 첫 번째 경우에는 문자열 대신 숫자를 전달했기 때문에 TypeError: data.split is not a function 오류가 발생합니다. any 유형 검사를 비활성화하므로 TypeScript는 오류를 강조 표시할 수 없습니다.
  • 두 번째 경우에는 res2 변수에도 any 유형이 있습니다. 이는 any 의 단일 사용이 코드베이스의 많은 부분에 계단식 영향을 미칠 수 있음을 의미합니다.


극단적인 경우나 프로토타입 제작이 필요한 경우 any 것을 사용해도 괜찮습니다. 일반적으로 TypeScript를 최대한 활용하려면 any 사용하지 않는 것이 좋습니다.

모든 유형이 나오는 곳

명시적으로 any 작성하는 것이 유일한 옵션은 아니기 때문에 코드베이스에서 any 유형의 소스를 알고 있는 것이 중요합니다. any 유형을 사용하지 않으려는 최선의 노력에도 불구하고 때때로 암시적으로 코드베이스에 몰래 들어갈 수 있습니다.


코드베이스에는 any 유형의 네 가지 주요 소스가 있습니다.

  1. tsconfig의 컴파일러 옵션.
  2. TypeScript의 표준 라이브러리.
  3. 프로젝트 종속성.
  4. 코드베이스에서 any 를 명시적으로 사용합니다.


나는 이미 tsconfig의 주요 고려 사항 과 처음 두 가지 사항에 대한 표준 라이브러리 유형 개선에 대한 기사를 작성했습니다. 프로젝트의 형식 안전성을 향상하려면 이 내용을 확인하세요.


이번에는 코드베이스에서 any 유형의 모양을 제어하는 자동 도구에 중점을 둘 것입니다.

1단계: ESLint 사용

ESLint 는 모범 사례와 코드 형식을 보장하기 위해 웹 개발자가 사용하는 널리 사용되는 정적 분석 도구입니다. 코딩 스타일을 적용하고 특정 지침을 따르지 않는 코드를 찾는 데 사용할 수 있습니다.


typectipt-eslint 플러그인 덕분에 ESLint를 TypeScript 프로젝트에서도 사용할 수 있습니다. 아마도 이 플러그인은 프로젝트에 이미 설치되어 있을 것입니다. 하지만 그렇지 않은 경우 공식 시작 가이드를 따를 수 있습니다.


typescript-eslint 의 가장 일반적인 구성은 다음과 같습니다.


 module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };


이 구성을 사용하면 eslint 구문 수준에서 TypeScript를 이해할 수 있으므로 코드에서 수동으로 작성된 유형에 적용되는 간단한 eslint 규칙을 작성할 수 있습니다. 예를 들어, 명시 any .


recommended 사전 설정에는 코드 정확성 향상을 목표로 신중하게 선택된 ESLint 규칙 세트가 포함되어 있습니다. 전체 사전 설정을 사용하는 것이 권장되지만 이 문서에서는 no-explicit-any 규칙에만 중점을 둘 것입니다.

명시적이지 않은 것

TypeScript의 엄격 모드는 묵시적인 any 사용을 금지하지만 명시적으로 any 사용을 금지하지는 않습니다. no-explicit-any 규칙은 코드베이스의 any 곳에도 수동으로 작성하는 것을 금지하는 데 도움이 됩니다.


 // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}


이 규칙의 주요 목적은 팀 전체에서 any 사용하지 못하도록 방지하는 것입니다. 이는 프로젝트에서 any 사용하지 않는 것이 좋다는 팀의 합의를 강화하는 수단입니다.


any 것을 한 번만 사용해도 유형 추론 으로 인해 코드베이스의 상당 부분에 연쇄적인 영향을 미칠 수 있으므로 이는 중요한 목표입니다. 그러나 이는 여전히 궁극적인 유형 안전성을 달성하는 것과는 거리가 멀습니다.

명시적이지 않은 것만으로는 충분하지 않은 이유

명시적으로 사용된 any 다루었지만 npm 패키지 및 TypeScript의 표준 라이브러리를 포함하여 프로젝트의 종속성 내에 암시된 any 여전히 많이 있습니다.


모든 프로젝트에서 볼 수 있는 다음 코드를 고려해보세요.


 const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any


변수 pokemonssettings 모두 암시적으로 any 유형이 지정되었습니다. 이 경우 no-explicit-any 나 TypeScript의 엄격 모드는 우리에게 경고하지 않습니다. 아직 아님.


이는 response.json()JSON.parse() 의 유형이 TypeScript의 표준 라이브러리에서 오기 때문에 발생합니다. 여기서 이러한 메서드에는 명시적인 any 주석이 있습니다. 변수에 대해 더 나은 유형을 수동으로 지정할 수 있지만 표준 any 에는 거의 1,200번의 유형이 있습니다. 표준 라이브러리에서 우리 코드베이스로 any 들어올 수 있는 모든 경우를 기억하는 것은 거의 불가능합니다.


외부 종속성도 마찬가지입니다. npm에는 형식이 잘못된 라이브러리가 많이 있으며 대부분은 여전히 JavaScript로 작성됩니다. 결과적으로 이러한 라이브러리를 사용하면 코드베이스에 많은 암시적 any 쉽게 발생할 수 있습니다.


일반적으로 우리 코드에 any 들어갈 수 있는 방법은 여전히 많습니다.

2단계: 유형 검사 기능 강화

이상적으로는 어떤 이유로든 any 유형을 받은 모든 변수에 대해 컴파일러가 불평하도록 TypeScript에 설정을 갖고 싶습니다. 불행하게도 그러한 설정은 현재 존재하지 않으며 추가될 것으로 예상되지 않습니다.


typescript-eslint 플러그인의 유형 확인 모드를 사용하여 이 동작을 달성할 수 있습니다. 이 모드는 TypeScript와 함께 작동하여 TypeScript 컴파일러에서 ESLint 규칙까지 완전한 유형 정보를 제공합니다. 이 정보를 사용하면 TypeScript의 유형 검사 기능을 기본적으로 확장하는 더 복잡한 ESLint 규칙을 작성할 수 있습니다. 예를 들어, 규칙은 any 를 얻은 방법에 관계없이 any 유형의 모든 변수를 찾을 수 있습니다.


유형 인식 규칙을 사용하려면 ESLint 구성을 약간 조정해야 합니다.


 module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };


typescript-eslint 에 대한 유형 추론을 활성화하려면 ESLint 구성에 parserOptions 추가하세요. 그런 다음 recommended 사전 설정을 recommended-type-checked 로 바꿉니다. 후자의 사전 설정은 약 17개의 새로운 강력한 규칙을 추가합니다. 이 기사에서는 그 중 5가지에만 중점을 둘 것입니다.

안전하지 않은 인수

no-unsafe-argument 규칙은 any 유형의 변수가 매개변수로 전달되는 함수 호출을 검색합니다. 이런 일이 발생하면 유형 검사가 손실되고 강력한 유형 지정의 모든 이점도 손실됩니다.


예를 들어, 객체를 매개변수로 요구하는 saveForm 함수를 생각해 보겠습니다. JSON을 수신하여 구문 분석하고 any 유형을 얻는다고 가정합니다.


 // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.


이 매개변수를 사용하여 saveForm 함수를 호출하면 no-unsafe-argument 규칙은 이를 안전하지 않은 것으로 플래그 지정하고 value 변수에 대해 적절한 유형을 지정하도록 요구합니다.


이 규칙은 함수 인수 내에 중첩된 데이터 구조를 심층적으로 검사할 만큼 강력합니다. 따라서 객체를 함수 인수로 전달하면 형식화되지 않은 데이터가 절대 포함되지 않을 것이라고 확신할 수 있습니다.


 // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });


오류를 해결하는 가장 좋은 방법은 TypeScript의 유형 축소Zod 또는 Superstruct 와 같은 유효성 검사 라이브러리를 사용하는 것입니다. 예를 들어, 구문 분석된 데이터의 정확한 유형을 좁히는 parseFormValues 함수를 작성해 보겠습니다.


 // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);


알 수 unknown 허용하는 함수에 대한 인수로 any 유형을 전달하는 것이 허용됩니다. 그렇게 하는 것과 관련된 안전 문제가 없기 때문입니다.


데이터 유효성 검사 함수를 작성하는 것은 지루한 작업이 될 수 있으며, 특히 대량의 데이터를 처리할 때 더욱 그렇습니다. 따라서 데이터 검증 라이브러리의 사용을 고려해 볼 가치가 있습니다. 예를 들어 Zod를 사용하면 코드는 다음과 같습니다.


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);


안전하지 않은 할당

no-unsafe-assignment 규칙은 값이 any 유형인 변수 할당을 검색합니다. 이러한 할당은 컴파일러가 변수에 특정 유형이 있다고 생각하도록 오해할 수 있지만 데이터는 실제로 다른 유형을 가질 수 있습니다.


JSON 구문 분석의 이전 예를 고려하십시오.


 // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value


no-unsafe-assignment 규칙 덕분에 formValues 다른 곳에 전달하기 전에도 any 유형을 포착할 수 있습니다. 수정 전략은 동일하게 유지됩니다. 유형 축소를 사용하여 변수 값에 특정 유형을 제공할 수 있습니다.


 // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues


안전하지 않은 회원 액세스 및 안전하지 않은 호출 없음

이 두 규칙은 훨씬 덜 자주 실행됩니다. 그러나 내 경험에 따르면 형식이 잘못된 타사 종속성을 사용하려고 할 때 정말 도움이 됩니다.


no-unsafe-member-access 규칙은 변수에 any 유형이 있는 경우 null 이거나 undefined 수 있으므로 객체 속성에 액세스하지 못하게 합니다.


no-unsafe-call 규칙은 함수가 아닐 수도 있는 any 유형의 변수를 함수로 호출하는 것을 방지합니다.


untyped-auth 라는 잘못된 유형의 타사 라이브러리가 있다고 가정해 보겠습니다.


 // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.


Linter는 두 가지 문제를 강조합니다.

  • authenticate 함수를 호출하는 것은 중요한 인수를 함수에 전달하는 것을 잊어버릴 수 있으므로 안전하지 않을 수 있습니다.
  • userInfo 개체에서 name 속성을 읽는 것은 인증이 실패하면 null 이 되므로 안전하지 않습니다.


이러한 오류를 해결하는 가장 좋은 방법은 강력한 형식의 API가 포함된 라이브러리 사용을 고려하는 것입니다. 그러나 이것이 옵션이 아닌 경우라이브러리 유형을 직접 늘릴 수 있습니다. 고정 라이브러리 유형의 예는 다음과 같습니다.


 // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }


안전하지 않은 반품

no-unsafe-return 규칙은 좀 더 구체적인 것을 반환해야 하는 함수에서 실수로 any 유형을 반환하지 않도록 도와줍니다. 이러한 경우 컴파일러는 반환된 값에 특정 유형이 있다고 잘못 생각하지만 데이터에는 실제로 다른 유형이 있을 수 있습니다.


예를 들어 JSON을 구문 분석하고 두 가지 속성이 있는 객체를 반환하는 함수가 있다고 가정해 보겠습니다.


 // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null


parseForm 함수는 구문 분석된 값을 확인하지 않기 때문에 사용되는 프로그램의 모든 부분에서 런타임 오류가 발생할 수 있습니다. no-unsafe-return 규칙은 이러한 런타임 문제를 방지합니다.


구문 분석된 JSON이 예상 유형과 일치하는지 확인하는 유효성 검사를 추가하면 이 문제를 쉽게 해결할 수 있습니다. 이번에는 Zod 라이브러리를 사용해 보겠습니다.


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }


성능에 대한 참고 사항

유형 확인 규칙을 사용하면 모든 유형을 추론하기 위해 TypeScript의 컴파일러를 호출해야 하기 때문에 ESLint에 대한 성능 저하가 발생합니다. 이러한 감속은 주로 커밋 전 후크와 CI에서 linter를 실행할 때 눈에 띄지만 IDE에서 작업할 때는 눈에 띄지 않습니다. 유형 검사는 IDE 시작 시 한 번 수행된 다음 코드를 변경하면 유형을 업데이트합니다.


단순히 유형을 추론하는 것이 일반적인 tsc 컴파일러 호출보다 빠르게 작동한다는 점은 주목할 가치가 있습니다. 예를 들어 약 150만 줄의 TypeScript 코드가 포함된 가장 최근 프로젝트에서 tsc 통한 유형 검사에는 약 11분이 소요되는 반면, ESLint의 유형 인식 규칙이 부트스트랩하는 데 필요한 추가 시간은 약 2분에 불과합니다.


우리 팀의 경우 유형 인식 정적 분석 규칙을 사용하여 추가적인 안전성을 제공하는 것은 그만한 가치가 있습니다. 소규모 프로젝트에서는 이 결정을 내리는 것이 훨씬 더 쉽습니다.

결론

TypeScript 프로젝트에서 any 사용을 제어하는 것은 최적의 유형 안전성과 코드 품질을 달성하는 데 중요합니다. typescript-eslint 플러그인을 활용하면 개발자는 코드베이스에서 any 유형의 발생을 식별하고 제거하여 더욱 강력하고 유지 관리 가능한 코드베이스를 만들 수 있습니다.


유형 인식 eslint 규칙을 사용하면 코드 베이스에 any 키워드가 나타나는 것은 실수나 실수가 아니라 의도적인 결정이 됩니다. 이 접근 방식은 표준 라이브러리 및 타사 종속성뿐만 아니라 자체 코드에서도 any 사용하지 못하도록 보호합니다.


전반적으로 유형 인식 린터를 사용하면 Java, Go, Rust 등과 같은 정적으로 유형이 지정된 프로그래밍 언어와 유사한 수준의 유형 안전성을 달성할 수 있습니다. 이는 대규모 프로젝트의 개발 및 유지 관리를 크게 단순화합니다.


이 기사에서 새로운 것을 배웠기를 바랍니다. 읽어 주셔서 감사합니다!

유용한 링크