최근 대다수의 자바스크립트 프로젝트는 타입스크리트 기반으로 작성되는 경우가 많다. 코드 작성환경에서 타입을 체크할 수 있는 타입스크립트는 코드를 더욱 안전하게 짤 수 이쎅 도움을 주고 잠재적인 버그들 또한 줄여주기 때문이다.
타입스크립트 ?
TypeScript is JavaScript with syntax for types.
기존 자바스크립트 문법에 타입을 가미한 것
JS는 기본적으로 동적 타입언어이다. 이는 편리하기도 하지만 대부분의 에러를 코드를 실행하는 단계에서 알 수 있다라는 단점이 있어 프로젝트 규모가 커질수록 발목을 잡게 된다는 단점이 존재한다.
아래 함수를 한번 보자.
function test(a,b) {
return a/b
}
test(5,2) // 2.5
test('안녕하세요','하이')// NaN
위 함수는 두수를 더해주는 함수인데 숫자가 아닌 문자가 들어가면 NaN이 나오게 된다. 만약 typeof 연산자를 활용한다면 아래와 같이 타입을 체크할 수 있다.
function test(a,b) {
if (typeof a !== 'number' || typeof b !== 'number'){
throw new Error('a,b 모두 숫자여야함!')
}
return a/b
}
하지만 위와같은 방법은 모든 함수와 변수에 적용하는 것은 너무 번거로울 뿐 아니라 코드의 크기를 늘리게 된다. 위 코드를 타입스크립트를 활용한다면 보다 간결하게 표현할 수 있다.
function test(a:number , b: number){
return a/b
}
위 경우 프로젝트를 실행하는 것이 아닌 만약 test 함수에 number가 아닌 다른값을 넣을때 빌드하는 시점에서 에러를 알려준다.
타입스크립트 이전에는 Flow라고 하는 정적 타입 체크 라이브러리가 존재했다. 슈퍼셋 언어라기 보다는 타이핑을 도와주는 라이브러리에 가까운 형태로 지원되어 있다. 리액트 코드 또한 이 Flow를 기반으로 내부 정적 타이핑에 도움을 얻고 있다.
VSCODE가 타입스크립트를 강력하게 지원하면서 같은 페이스북팀에서 만든 라이브러리인 Flow와 Jest또한 타입스크립트로 재작성되며 Flow로 작성된 프로젝트는 보기 힘들어졌다.
해당 책의 저자는 이런 JS생태계에 맞춰 대부분 예제코드 또한 타입스크립트로 작성했다고 한다. 리액트 또한 @types/react 라이브러리를 통해 타입스크립트로도 매끄럽게 리액트 코드를 작성할 수 있기 때문이다.
리액트에서의 타입스크립트
any 대신 unknown 사용하기
타입스크립트를 작성할 때 any를 최대한 지양해야 한다는 사실은 알고있다. any를 사용하는 것은 사실 타입 정의를 사용하지 않는다는 말과 똑같기 때문이다.
function doSomething(callback : any){
callback()
}
doSomething(1)
즉 타입을 any로 잡는다면 타입으로 발생하는 오류를 빌드단계에서 잡아낼 수 없게 된다.
만약 개발중에 아직 타입을 단정할 수 없는 단계라면 unknown을 사용하자. unknown또한 모든 값을 할당할 수 있지만 해당 값을 바로 사용하는 것은 불가능하게 해주는 타입이다.
function doSomething(callback : unknown) {
callback() //'callback' is of type 'unknown'
}
이 변수를 사용하기 위해서는 타입을 적절히 좁혀야 한다.
function doSomething(callback : unknown) {
if (typeof callback === 'function') {
callback()
return
}
throw new Error('callback은 함수여야 합니다')
}
unknown과 반대되는 개념으로 never가 있다. never타입은 unknown과 반대로 어떤 타입도 들어올 수 없음을 의미한다.
type what = string & number
위 경우처럼 어떤 교차점이 없는 타입을 선언하는 경우 never타입이 들어가게 된다.
실제 리액트 코드에서는 props는 없지만 state가 존재하는 상황에서 빈 props를 통해 어떤 props도 받지 않는다는 의미로 사용이 가능하다.
import React from "react";
import logo from "./logo.svg";
import "./App.css";
type Props = Record<string, never>;
type State = {
counter: 0;
};
class SampleComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
counter: 0,
};
}
render() {
return <>...</>;
}
}
function App() {
return (
<>
<SampleComponent />
<SampleComponent hello="world" /> //에러를 발생시킴
</>
);
}
export default App;
타입 가드
타입스크립트를 사용한다면 최대한 타입을 좁히는 쪽이 좋다.
instanceof <- 지정한 인스턴스가 특정 클래스의 인스턴스인지 확인할 수 있음
class UnExpectedError extends Error{
constructor() {
super()
}
get message () {
return '에러 발생!!'
}
}
async function func(){
try {
//무언가
} catch(e){
if(e instanceof UnExpectedError) {
//무언가
}
}
에러는 unknown으로 내려오는데 이를 타입 가드를 통해서 에러에 따라 다른 처리가 가능하다.
typeof <- 특정 요소의 자료형을 확인하는데 사용
typeof 3 === 'number'
in <- 어떤 객체에 키가 존재하는지 확인
interface Student {
age : number
}
function do(person : Student){
console.log('age' in person)
}
제네릭
제네릭은 함수나 클래스 내부에서 단일 타입이 아닌 다양한 타입에 대응할 수 있게 도와주는 도구이다.
하나의 타입으로 이루어진 배열의 첫번째와 마지막 요소를 반환하는 함수를 만든다고 생각해보자.
function getFirstAndLast(list : unknown[]) {
return [list[0],list[list.length -1]]
}
const [first,last] = getFirstAndLast([1,2,3,4,5])
first // unknown
last // unknown
타입이 정해져 있지 않기에 unknown을 사용했지만 결과물이 unknown이 나오게 된다. 이때 적용할 수 있는것이 제네릭이다.
function getFirstAndLast<T>(list : T[]):[T,T] {
return [list[0],list[list.length -1]]
}
const [first,last] = getFirstAndLast([1,2,3,4,5])
first // number
last // number
T라는 제네릭을 선언하고 배열의 타입과 반환값의 타입을 지정하는데 사용하였다. 이 제네릭을 활용하여 다양한 타입에 모두 적용시킬수 있는 함수를 만들 수 있다.
리액트에서 제네릭을 많이 사용하는 코드는 useState이다.
const [state,setState] = useState<string>('')
인덱스 시그니처
인덱스 시그니처란 객체의 키를 정의하는 방식이다.
type Hello = {
[key : string] : string
}
const hello : Hello = {
hello : 'hello',
hi : 'hi',
}
hello['hi'] // hi
hello['안녕'] // undefined
인덱스 시그니처를 사용하면 키에 원하는 타입을 부여할 수 있다. 단 위 예제에서는 key의 범위를 모든 string으로 굉장히 크게 설정했기에 undefined가 나왔다. 따라서 객체의 타입은 필요에 따라 좁히는 게 좋다.
type Hello = Record<'hello' | 'hi' , string>
type Hello = { [key in 'hello' | 'hi'] : string }
Record를 사용하면 객체의 타입에 원하는 키와 값을 넣을 수 있다. 혹은 인덱스 시그니처에 타입을 지정해주는 방법이 있다.
덕 타이핑
Object.keys 함수는 반환형이 string[]으로 고정되어 있다.
Object.keys(hello).map((key) => {
const value = hello[key]
return value
}
따라서 위 코드를 실행시키면 오류가 발생한다.
Object.keys는 useState와 같이 제네릭을 사용하지도 않는다. 이와 관련된 이슈가 실제로 많지만 모두 TS 관리자들에 의해 닫혔다고 한다.
JS는 다른 언어에 비해 객체가 열려 있는 구조로 만들어져 있다.
덕 타이핑
객체의 타입이 클래스 상속, 인터페이스 구현 등으로 결저오디는 것이 아닌 객체가 필요한 변수,메서드가 있다면 해당 타입에 속하도록 인정해 주는 것.
"어떤것이 오리처럼 걷고, 헤엄치고, 소리를 내면 오리라고 부를 수 있다."
실제로 타입스크립트 인터페이스 소개란에는 아래와 같은 문장이 있다.
타입스크립트의 핵심 원칙 중 하나는 타입 검사가 값의 형태에 초점을 맞춘다는 것이다.
JS는 객체의 타입에 구애받지 않고 객체의 타입에 열려 있기 때문에 타입스크립트 또한 이에 맞춰주기 위함이다. 즉 모든 키가 들어올 수 있는 가능성이 열려있는 객체 키에 대응하기 위해서 string[] 으로 타입을 제공한다.
일부 개발자들은 정확한 타입을 반환하는 Exact라는 새로운 타입을 요구하고 있으며 아직 해당 이슈는 닫혀있지 않으니 언젠간 나올수도 있다고 한다..
정리
타입스크립트를 사용하는 것 혹은 기존 자바스크립트 파일을 타입스크립트로 변환하는 것은 매우 인내심이 필요한 일이다. 하지만 타입스크립트를 활용하다보면 그동안 발견치 못했던 에러를 일으킬 수 있는 코드를 발견하고 코드가 매우 단단해질 것이라고 한다.
책의 저자는 타입스크립트를 매우 신봉하는 것 같아 보인다. 최근에 타입스크립트 관련 된 이슈는 참 많은것 같다.
https://blog.cosign.cc/?p=1057
https://www.clickittech.com/developer/why-use-typescript/
물론 타입스크립트를 쓰지 말자는 의견도 존재한다.
https://javascript.plainenglish.io/7-really-good-reasons-not-to-use-typescript-166af597c466
책의 저자도 타입스크립트는 슈퍼셋 언어로 JS를 먼저 이해하고 TS에 뛰어들기를 권장하고 있기 때문에 TS를 무작정 사용하기 보다는 JS에 대해 먼저 이해하고 무조건 사용할 필요는 없는 것 같다.
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(5) 가상 DOM & 리액트 파이버 (1) | 2023.12.18 |
---|---|
[React] Deep Dive 모던 리액트(4) JSX (1) | 2023.12.18 |
[React] DeepDive 모던리액트(2) 동등 비교 (0) | 2023.12.14 |
[React] DeepDive 모던 리액트 (1) 리액트의 역사 (0) | 2023.12.13 |
[JS] DeepDive(49) Babel과 Webpack을 이용한 ES6+/ES.NEXT 개발 환경 구축 (0) | 2023.09.27 |