타입스크립트의 any, unknown

서건혁·2024년 3월 13일
2
post-thumbnail

타입스크립트는 기존 자바스크립트에서 컴파일 과정에서 타입을 추론하고 이에 대해 사용자에게 오류를 알려줄 수 있는 자바스크립트의 수퍼셋 언어이다.

이러한 타입스크립트를 사용하면 기존 자바스크립트 사용자가 타입 없이 코드를 작성하며 생길 수 있는 오류를 런타임 시간이 아닌 컴파일 시간에 조기 감지하고 해당 오류를 런타임 전에 해결함으로써 실제 서비스 중에 생길 수 있는 장애를 줄일 수 있다는 장점을 가지고 있다.

하지만 우리는 실제로 타입스크립트를 쓸 때 원시값을 위주로 쓰지, 그 외의 타입을 제대로 쓰지는 않는다. 그 외의 타입을 써봤자 api를 연결할 때 쓰는 any 타입이나 상수 정의를 하기 위한 Enum 타입 정도일 것이다.

물론 나도 타입스크립트를 사용할 때 그것을 위주로 쓰는 편이다. 하지만 이번에는 더 나아가서 실제 개발자들이 잘 쓰지 않는 타입이며, 어떻게 보면 생소할 수 있는 타입인 any, unknown 타입에 대해서 알아보고 이를 실제로 어떻게 사용하는지 알아보는 시간을 갖고자 한다.

any

아까 위에서 언급했던 말을 잠깐 빌리겠다.

우리가 자바스크립트를 사용할 때, 사용자의 ID를 쿼리스트링에 담아 api에 요청하면 해당 api가 유저 정보를 반환해준다고 가정해보자.

//사용자의 ID를 활용하여 유저 정보를 가져오는 API
const connectionApi = async(userId) => {
	try{
		const response = await fetch(`${api주소}/userinfo?userId=${userId}`)
														.then((result) => result.json());
	}
	catch(error){
		console.error(error);
		//..실패했을 때 코드 작성
	}
}

그럼 자바스크립트에서는 분명 코드를 이런 식으로 짤 것이다.

자 그럼 이것을 타입스크립트로 변환하고 싶다면, 어떻게 해야 할까?

response에는 무슨 타입을 써야 할까? 그리고 return은 어떤 타입을 선언해야 할까?

string 배열로 선언하면 될까? 아니 애초에 그 정보가 반드시 string 배열로만 이루어질 것이라는 보장이 있을까?

유저 정보에 만약 유저 프로필 사진 파일을 가져오는 내용이나 숫자도 입력되어 있다면, 과연 이때는 어떻게 할 것인가?

고민이 많아진다. 이때가 바로 타입스크립트의 타입 강제성이 개발자를 속박하는 순간이다.

물론 typescript 설정에서 타입을 강제하는 옵션을 끌 수도 있다.

//tsconfig.json
{
  "compilerOptions": {
    "strict": true // false로 설정하면 타입을 강제로 선언할 필요가 없다.
  }
}

하지만 이걸 끄게 될 경우 과연 타입스크립트를 사용할 의미가 있어질까? 그렇다면 어쩌라는 거냐….

이런 사람들을 위해 준비했다. 바로 any 타입이다.

any 타입은 기존 자바스크립트에서 타입 없이 변수를 선언하고 그대로 사용하는 것처럼 만들어 준다.

조금 더 기술적인 내용을 덧붙인다면, 타입스크립트에서 해당 변수에 들어갈 타입에 대해 모든 타입을 허용해 준다는 뜻이다.

//예시
const a : any = ''; //가능
cnsole.log(a);
const b : any = 0; //가능
console.log(b);
const c : any = false; //가능
console.log(c);
const d : any = new CustomClass(); //커스텀 클래스나 타입에 관해서도 가능하다.
console.log(d);
//심지어 이런 것도 가능하다.

const e : any & number = 3 //가능
console.log(e);
const f : any & number = '' //가능 왜?
console.log(f);

(로그는 6번째부터 보면 된다)

마지막이 가능한 이유는 any 타입이라서다. 실제로 & 연산자는 타입의 교집합 역할을 하는데, any타입과 & 연산자가 만나면 그 뒤에 나오는 타입이 어떠한 것이든 any 타입으로 변질된다.

물론 실제로 잘 사용하지 않는 유형이며, 이것을 알려주는 이유는 단지 any 타입이 이러한 특수한 경우를 가지고 있다는 것을 보여주기 위해서이다.

이렇게 만능의 any 타입을 알아보았다. 그럼 아까 위에서 작성된 자바스크립트 코드를 타입스크립트로 쓴다면?

//사용자의 ID를 활용하여 유저 정보를 가져오는 API
const connectionApi = async(userId: string): Promise<any> => {
	try{
		const response: any = await fetch(`${api주소}/userinfo?userId=${userId}`)
														.then((result) => result.json());
	}
	catch(error){
		console.error(error);
		//..실패했을 때 코드 작성
	}
}

이러한 형식이 나올 것이다.

방금 예시처럼 이렇게 타입을 추론하기 어려운 API 통신을 연결하는 과정에서 자주 사용한다. 그 외에는 타입이 자주 변할 수 있는 변수여야 하거나, 아니면 자바스크립트에서 타입스크립트로 이전하는 과정같이 기존 자바스크립트 코드로 인해 타입 추론이 어려운 경우에도 사용할 수 있을 것이다.

하지만 많은 개발자들이 그 외에는 쓰지 않는 것이 좋다고 말하고 있다.

왜냐하면 any 타입으로 선언된 변수는 어떠한 값으로든 초기화 또는 재할당 될 수 있으며, 이러한 any 타입이 남발될 경우 그냥 타입스크립트를 가장한 자바스크립트 코드를 작성하는 것이기 때문이다.

심지어 이걸 비꼬는 용어인 ‘anyscript’라는 단어도 있다…

비꼰다는 걸 보면, 딱히 기술적 설명이 없어도 ‘아 이건 사용하지 말아야겠다’라는 생각이 들 것이다.

(???: 야 쟤 애니 좋아한대…)

아무튼 any 타입의 빈번한 사용은 개발자 간의 코드 컨벤션을 와해시킴과 동시에 타입 추론을 어렵게 하여 특정 타입 함수의 잘못된 사용을 초래한다. 그리고 이것이 문제가 된다.

자동완성 도움을 받을 수 없다.

어떤가? 타입스크립트를 쓰고 있는 개발자라면 any 없는 코드의 세상. 당신도 마주하고 싶지 않은가?

나도 그러고 싶지만 현실은 개같이 쓰고 있다. 미안하다. 내가 말했는데 개같이 쓰고 있다!

나 애니 좋아하는 거 같다.

농담이고, 이제 문제를 인식했으면 해결할 차례다.

unknown

해결할 차례라면서 왜 다음 단락으로 넘어가고 있는지 궁금해 할 것이다.

그 이유는 이제부터 배울 unknown이 아까 위에서 말했던 any를 써야 하는 상황에 대한 새로운 구세주가 될 수 있기 때문이다.

unknown은 any 버전의 제약판이라고 생각하면 되겠다.

any에서는 모든 타입에 대한 할당이 가능하며 특정 타입에 대한 함수 사용 또한 가능하다.(toUpperCase() 같은 거)

그리고 이러한 any 타입의 사용은 개발자가 변수를 다룰 때 어떻게 다뤄야 할 지 애매해지는 결과를 낳는다. any 타입일 때는 보시다시피 자동 완성도 지원되지 않는다.

그러므로 개발자들은 생각했다. 아 타입이 명확해야 개발이 편해지지 않을까? 근데 any 타입은 그걸 강제할 수 있는 방법이 없는데?

그래서 타입스크립트에서는 unknown 타입을 만들어 any 처럼 모든 타입에 대한 값을 할당 받을 수 있게 하되, 해당 변수에 할당되어 있는 함수를 막으려고 노력한다(이 부분이 중요하다.). 이럼으로써 사용자가 타입 변환을 명시적으로 하게끔 만든다.

무슨 말인지 이해를 못하겠다고? 코드를 보자.

//예시
const a : any  = 'wow'
console.log(a); //wow
console.log(a.toUpperCase()); //WOW

const b : unknown = 'wow';
console.log(b); //wow
console.log(b.toUpperCase()); //될까 과연?

된다. ㅋㅋㅋㅋㅋㅋㅋㅋ(unknown: 노력했잖아. 한잔해~)

하지만… 뭔가 다르지 않은가?

저 unknwon 변수 쪽에는 toUpperCase() 함수를 사용할 때 빨간 선이 뜬다.

사실 컴파일 에러도 안 나고, 실행도 된다. 하지만 코드를 작성하는 중에 이를 감지해준다.

여러분은 상사한테 코드 리뷰를 받을 때 적어도 빨간 줄은 안 보이게 하고 싶지 않은가?

이렇게 unknown 타입으로 초기화 된 변수를 바로 사용한다면, 빨간 줄이 보이면서 상사한테 욕을 먹을 것이다. (unknwon이 타입 사용을 강제하는 것이 아니라 상사가 타입 사용을 강제한다.)

그럼 저 빨간 줄을 어떻게 없애냐고?

//예시
const b : unknown = 'wow';
console.log(b);
const c = b as string; // 또는 <string> b
console.log(c.toUpperCase());

이런 식으로 Type Assertion을 통한 타입 변환 후 사용하면 된다.

💪알아두면 좋은 타입스크립트 지식
Type Assertion의 방법은 2가지가 있다. 하나는 <타입>변수 형태로 지정하는 것이고, 다른 하나는 방금 예시처럼 as를 사용하는 것이다. 필자는 as 문법을 사용하는 것을 추천한다. 만약 프론트엔드 개발자라면 타입스크립트를 사용할 때 높은 확률로 tsx를 사용하게 될텐데, 그럼 형태의 사용이 리액트 컴포넌트의 사용법과 구분이 어려울 수도 있기 때문이다.

아니면 이런 식으로 사용할 수도 있다.

const b : unknown = 'wow';
if (typeof b === 'string')
	console.log(b.toUpperCase());
else
	console.log(b);

지금까지 간단하게 unknwon에 대해서 알아보았다.

좋지 않은가? 그럼 이제부터 api 코드에서 any 타입을 없애보도록 하자.

우선 먼저 typescript 설정에서 코드에서 any가 존재하지 않게 만들자.

//tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true, //true일 경우 any 타입의 비명시적인 사용을 허용하지 않음. false는 허용
  }
}

해당 옵션을 켜도 any 옵션을 사용하지 못하는 것은 아니다. 하지만 암시적인 any 타입의 사용을 막아주는 역할을 한다.

자 그럼 옵션도 켰으니 한 번 고쳐보자.

//사용자의 ID를 활용하여 유저 정보를 가져오는 API
//string[]을 받아온다고 가정
const connectionApi = async(userId: string): Promise<string[] | undefined> => {
	try{
		const response: unknown = await fetch(`${api주소}/userinfo?userId=${userId}`)
        												.then((result) => result.json());
    const refinedResponse = response as string[];
    //무언가 수행
    return refinedResponse;
	}
	catch(error){
		console.error(error);
        return undefined;
		//..실패했을 때 코드 작성
	}
}

어떤가? 간편하게 고치지 않았나?

이런 식으로 any 타입을 제한할 수 있다. 여러분들도 any 타입을 탄압하고 unknown 타입을 적극 차용했으면 좋겠다.

출처

타입 표명(Type Assertion)
타입스크립트: any와 unknown
우리 팀의 우아한 타입스크립트 컨벤션 정하기 여정 | 우아한형제들 기술블로그

0개의 댓글