최근에 대다수의 프로젝트가 타입스크립트 기반으로 작성되었다. 코드를 작성하며 타입을 체크할 수 있는 타입스크립트는 코드를 더욱 안전하게 작성하면서도 잠재적인 버그도 크게 줄일 수 있다.
타입스크립트란?
기존 자바스크립트 문법에 타입을 가미한 것이 타입스크립트라 할 수 있다.
동적 타입의 언어인 Javascript는 런타임에 에러를 확인할 수 있으며 동적 타입 언아라는 점은 개발자에게 자유를 줄 수 있지만 코드의 규모가 커질수록 오히려 발목을 잡는 경우도 있다.
function test(a,b) {
return a/b
}
test(5,2) // 2.5
test('안녕하세요','하이')// NaN
두 인수를 받아 나눗셈한 결과를 반환하는데, 숫자가 아닌 타입의 값이 들어오면 NaN이 반환된다. 이러한 문제는 Javascript의 typeof를 이용하면 타입 체크를 할 수 있다.
function test(a,b) {
if (typeof a !== 'number' || typeof b !== 'number'){
throw new Error('a,b 모두 숫자여야함!')
}
return a/b
}
하지만 typeof를 적용해 체크하는 것은 너무 번거롭고 코드의 크기를 과도하게 키우게 된다. 위의 코드를 Typescript에서 다음과 같이 간결하게 표현할 수 있다.
function test(a:number , b: number){
return a/b
}
위의 예제에서 a와 b 변수에 number 타입을 지정하면 굳이 런타임까지 가지 않더라도 코드를 빌드하는 시점에 에러가 발생할 수 있는 코드를 확인할 수 있다.
타입스크립트의 등장 이전에는 Flow라는 정적 타입 체크 라이브러리가 있었다. Flow는 슈퍼셋 언어라기 보다는 타이핑을 도와주는 라이브러리에 가깝다. 리액트도 Flow를 기반으로 내부 정적 타이핑에 도움을 얻고 있다.
하지만 VSCode의 강력한 타입스크립트 지원과 Flow와 Jest 또한 타입스크립트로 재작성되며 사실상 타입스크립트가 Flow 대신 대세로 자리잡고 있다.
리액트 코드를 효과적으로 작성하기 위한 타입스크립트 활용법
any 대신 unknown을 사용하자
타입스크립트를 처음 작성할 때 저지르는 실수 중 하나는 any를 자주 사용한다는 것이다. any를 사용한다는 것은 사실상 타입스크립트가 제공하는 정적 타이핑의 이점을 모두 버리는 것이다.
function doSomething(callback : any){
callback()
}
doSomething(1)
위의 함수의 인수의 타입은 any로 되어 있어서 실제 함수가 아닌 값이 들어가도 타입스크립트가 에러를 발생시키지 않는다.
대신 불가피하게 아직 타입을 단정할 수 없는 경우에는 unknown을 사용하는 것이 좋다. unknown은 어떠한 값도 할당할 수 있지만 any와는 다르게 이 값을 바로 사용하는 것은 불가능하다.
function doSomething(callback : unknown) {
callback() //'callback' is of type 'unknown'
}
callback은 unknown, 즉 아직 알 수 없는 값이기 때문에 사용할 수 없다는 내용이다. type narrowing, 즉 타입을 원래 의도했던 대로 적절히 좁혀야 한다.
function doSomething(callback : unknown) {
if (typeof callback === 'function') {
callback()
return
}
throw new Error('callback은 함수여야 합니다')
}
이렇게 unknown을 사용하는 것은 예상치 못한 타입을 받아들일 수 있음은 물론 사용하는 쪽에서도 더욱 안전하게 사용할 수 있다.
unknown과는 반대로 never타입이 있는데, never는 어떠한 타입도 들어올 수 없음을 의미한다.
type what1 = string & number
type what2 = ('hello' | 'hi') & 'react'
양쪽 두 타입에는 교차점이 없기 때문에 이 경우에도 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와 typeof
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
in은 property in object로 사용되는데 어떤 객체에 키가 존재하는지 확인하는 용도로 사용된다.
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
인덱스 시그니처를 사용하면 키에 원하는 타입을 부여할 수 있다. 단 위의 예제의 경우 키의 범위가 string으로 너무 커지기 때문에 존재하지 않는 키로 접근하면 위와 같이 undefined를 반환할 수 있기에 객체의 타입도 필요에 따라 좁혀야 한다.
객체의 키는 동적으로 선언되는 경우를 최대한 지양해야 한다.
type Hello = Record<'hello' | 'hi' , string>
type Hello = { [key in 'hello' | 'hi'] : string }
Record<Key, Value>를 사용하면 객체의 타입에 각각 원하는 키와 값을 넣을 수 있으며 인덱스 시그니처에 타입을 사용함으로써 객체를 원하는 형태롤 최대한 좁힐 수 있다.
덕 타이핑
Object.keys 함수는 string[]으로 반환이 고정되어 있다.
Object.keys(hello).map((key) => {
const value = hello[key]
return value
}
그로인해 위의 코드를 실행하면 에러가 발생한다.
Object.keys는 useState와 같이 제네릭을 사용하지 않는다. 이와 관련된 이슈가 실제로 굉장히 많다.
Javascript는 다른 언어에 비해 객체가 열려 있는 구조로 만들어져 있어 덕 타이핑으로 객체를 비교해야 하는 특징이 있다.
덕 타이핑
객체의 타입이 클래스 상속, 인터페이스 구현 등으로 결정된느 것이 아닌 필요한 변수와 메서드만 지니고 있다면 그냥 해당 타입에 속하도록 인정해 주는 것.
타입스크립트 인터페이스 소개란에는 다음과 같은 문장이 등장한다.
타입스크립트의 핵심 원칙중 하나는 타입 검사가 값의 형태에 초점을 맞춘다는 것이다. 이를 "덕 타이핑" 또는 "구조적 서브타이핑"이라고 부르기도 한다.
Javascript는 객체의 타입에 구애받지 않고 객체의 타입에 열려 있으므로 타입스크립트도 이에 맞춰주기 위함이다. 즉 모든 키가 들어올 수 있는 가능성이 열려 있는 객체의 키에 포괄적으로 대응하기 위해 string[]으로 타입을 제공하는 것이다.
정리
웹 애플리케이션 개발에서 타입스크림트의 중요성은 갈수록 커지고 있지만 자바스크립트에서 타입스크립트로 바꿔나가는 것은 번거로움과 수고롭다. 하지만 이러한 문제가 있더라도 에러를 유발하는 코드를 발견하는 것과 코드의 효율성을 더욱 좋아질 것이다.
'React > 모던 리액트 Deep Dive' 카테고리의 다른 글
리액트의 렌더링과 메모이제이션 (1) | 2024.11.08 |
---|---|
클래스 컴포넌트와 함수 컴포넌트 (0) | 2024.11.05 |
가상 DOM과 리액트 파이버 (2) | 2024.10.30 |
[React Deep Dive] JSX(Javascript XML) (0) | 2024.10.26 |
[React Deep Dive] 동등 비교 (0) | 2024.10.08 |