typescript에는 특정 객체의 타입을 변경할 수 있는 키워드로 "as"와 "is"를 제공한다. 이 중 as는 꽤나 친숙한데, ts를 처음 접했을 때 흔히 "as any"를 남발하고는 하기 때문이다.
하지만 as 키워드는 함부로 사용할 경우 런타임 에러를 유발하고 디버깅도 어렵게 만들기 때문에 주의깊게 사용해야만 한다.
const a = getSomeValue() as string[];
console.log(a.length) // getSomeValue가 어떤 함수인지도 모르고, 나중에 어떻게 바뀔지 모르기 때문에 섣부르게 타입을 단언하는 경우 문제가 발생하기 쉬운 코드가 된다.
반면 is 키워드를 사용하면 이런 리스크를 줄일 수 있게 되는데, is는 어떻게 사용되는지 예시로 살펴보겠다.
우리가 구현하는 Card Component에 BigCard와 SmallCard 두 종류가 있다고 가정해보자. 이 두 Card 컴포넌트는 각각 BigInfo와 SamllInfo를 렌더링하는데 사용된다.
interface BaseInfo {
...
}
interface BigInfo extends BaseInfo {
bigPhoto: string;
}
interface SmallInfo extends BaseInfo {
smallPhoto: string;
}
이제 이 데이터를 리스트에 렌더링한다고 가정해보자.
const infoArr: (BigInfo | SmallInfo)[] = [...];
return <List>
{infoArr.map((item)=>{
if(item.bigPhoto){
return <BigCard card={item as BigInfo}/>
}
if(item.smallPhoto){
return <SmallCard card={item as SmallInfo}/>
}
return null
})}
</List>
위와같이 bigPhoto와 smallPhoto의 존재 여부를 기준으로 조건문을 주고, 거기에 맞춰 컴포넌트를 렌더링하는데, item은 여전히 BigInfo | SmallInfo 타입을 갖고 있기 때문에 BigInfo를 받는 BigCard 컴포넌트의 prop으로 적절하지 않다고 나온다. 이 문제를 해결하기 위해서 "as"를 이용해 type casting을 하는걸 볼 수 있다. 굉장히 직관적이지 않고, 코드도 장황해졌다. 지금은 하나의 필드로 값이 구분되는 간단한 케이스지만 만약 조금더 세분화되어서 구분되어야하는 타입이라면 조건문도 꽤나 어지러워질 것 같다.
이럴 때 "is"를 적용해 볼 수 있다. is는 타입 가딩 함수를 통해 하위 스코프에서 타입을 지정해줄 수 있다.
const isBigInfo = (info:any): info is BigInfo => {
return !!info.bigPhoto
}
const isSmallInfo = (info:any): info is SmallInfo => {
return !!info.smallPhoto
}
const infoArr: (BigInfo | SmallInfo)[] = [...];
return <List>
{infoArr.map((item)=>{
if(isBigInfo(item)){
// 이 스코프에서 item의 타입은 BigInfo.
return <BigCard card={item}/>
}
if(isSmallInfo(item)){
// 이 스코프에서 item의 타입은 SmallInfo.
return <SmallCard card={item}/>
}
return null
})}
</List>
코드도 훨씬 깔끔해졌고, 확장성에도 훨씬 자유로워졌다.
is는 이런 케이스 외에도 다양하게 활용 가능하다. webview 환경에서 message로 통신할 때 앱과 웹 사이드에서 미리 정해둔 메시지 형식이 있다고 가정해보자. 기본적으로 웹-앱간 주고받는 message는 string 타입을 갖고, 이는 object 타입을 직렬화한 형태일 확률이 높다. 이렇게 애매모호하고 어떤 값이 들어올지 모르는 환경에서는 어디서 문제가 생길지 몰라 굉장히 불안할 수밖에 없다. 이럴 때 아래와 같이 미리 타입을 선언해두고 is로 판단함으로써 사이드이펙트를 최소화할 수 있다.
type WebViewMssage = {
TALK: {
eventName: 'TALK';
data: {status: boolean; message?: string};
}
}
const isTalkMessage = (message:any): message is WebViewMssage['TALK'] => {
return message?.eventName === 'TALK'
}
const onMessage = (event: WebViewMessageEvent) => {
const message = JSON.parse(event.nativeEvent.data);
if(isTalkMessage(message)){
// 이 스코프에서 message는 WebViewMssage['TALK']의 타입을 가짐
// evnetName과 data의 타입을 특정할 수 있으므로 ide의 도움을 받아 실수할 여지를 줄여준다.
...
}
}
언노운 쓰십쇼