간단기법

동적인 데이터를 처리하는 다양한 방법.

씨알메리 2023. 3. 13. 13:16

동적인 데이터를 다뤄야 하는 상황

서버가 클라이언트로부터 데이터를 받는 지점이라 거나, npm jwt 라이브러리와 타입스크립트를 같이 사용하다 보면, 타입을 어떻게 해야 할지 고민이 생기는 곳이 있다.

// jwt는 내가 원하는 키:밸류 를 이용하여 서명을하고 검증을 할 수 있다.

token = jwt.sign({id: "1234"}, secret)
// 을 통해 얻은 token을 쿠키에 담아 클라이언트로 응답한다.

// 클라이언트는 서버에 요청할 때마다 쿠키를 같이 보내게 되고, 서버는 쿠키에서 토큰을 꺼내 검증한다.
result = jwt.verify(token, secret);

// 이 때, verify로 얻은 result에 id가 있는지 아닌지는 verfiy의 리턴 타입만으론 알 수 없다.
// verify의 리턴은 다음과 같다.

export interface JwtPayload {
    [key: string]: any;
    iss?: string | undefined;
    sub?: string | undefined;
    aud?: string | string[] | undefined;
    exp?: number | undefined;
    nbf?: number | undefined;
    iat?: number | undefined;
    jti?: string | undefined;
}

위 코드를 보면, JwtPayload 타입만으로는 jwt에 서명할 때 어떤 키를 사용했는지 알 수 없다. 익스프레스를 사용할 때, 리퀘스트의 바디에서 데이터를 뽑을 때도 동일하다. 외부(파일, 서버 등)로부터 입력을 받는 가장 경계선에선 항상 발생하는 상황이다. 그렇다면, 어떻게 타입을 명확하게 나타낼 수 있을까.

방법1 타입 단언(type assertion, casting)

1. 타입 단언(타입 캐스팅)
interface IPayload extends JwtPayload {
	email: string;
}

result = jwt.verify(token, secret) as IPayload;

// 이후엔 result.email로 접근하여 사용할 수 있다.
// 하지만, 타입 캐스팅이기에 result에 정말 email 프로퍼티가 있는지는 모른다
// 다음 예제 코드를 보면, result.email이 없는게 확실하지만, 타입 단언은 이를 무시한다.
interface A {
    id: string;
}

interface B extends A {
    email: string;
}

function GetA(): A {
    return {id: "MyId"};
}

const result = GetA() as B;
console.log(result.email); // undefined

방법2 in 연산자

in 연산자는 해당 key가 오브젝트에 존재하는 확인하는 연산자이다. in 연산자를 이용하면 type narrowing과 함께 실제로 객체 안에 원하는 프로퍼티가 있는지 확인할 수 있다.

interface A {
    id: string;
}

interface B extends A {
    email: string;
}

function GetA(): A {
    return {id: "MyId"};
}

const result = GetA();
if('email' in result) {
    console.log(result.email);
}

방법3 type predicate

방법2는 in 연산자를 활용하였다. 방법3 type predicate는 리턴 타입을 '파라미터 is 타입'으로 작성하는 방식이다.

interface IPayload extends Payload {
    id: string;
}

function hasId(obj: any): obj is IPayload {
    return (obj.id !== undefined);
}

const decoded = jwt.verify(token, process.env.JWT_SECRET!);
if(hasId(decoded)){
	// 타입이 좁혀져서 decoded.id 와같이 접근 가능하다
    decoded.id  = ...
}

방법4 assertion signature

리턴 타입을 이용하는 점에서 방법3과 비슷하다. 타입스크립트 3.7에 새로 소개된 기능으로, 다음과 같이 사용한다.

function assert(condition: any, msg?: string): asserts condition {
	if (!condition) {
    	throw new AssertionError(msg);
    }
}

function yell(str) {
	assert(typeof str === "string");
    // assert 이후로 str은 string으로 확실하게 좁혀진다
    
    //아래 함수는 철자가 잘못되었으므로 타입스크립트 컴파일러가 에러를 잡아준다.
    return str.toUppercase();
}

 

결론

위의 방법들 많고도 더 있어 보인다. 방법이 너무 많다. 이러한 방법을 모두 외우고 기억하는 것은 좋은 방향은 아닌 것 같다. 중요한 것은 타입을 한정 지을 수 있는 어떤 구문을 만나면(타입가드) 타입스크립트 컴파일러는 이를 분석하고 그다음 코드에서 타입을 좁힐 수 있다(타입내로잉)는 원리를 이해하고 자주 써보는 것으로 보인다.

 

타입 캐스팅이 가장 간단하지만, 객체에 내가 원하는 프로퍼티가 없을 수도 있으므로 확인하는 코드가 필요하다. 이러한 코드는 'key1' in obj, 'key2' in obj... 와 같은 코드 진행을 갖기에 번거로운 부분이 있다. class-transformer, class-validator 같은 라이브러리가 선호된 이유이기도 하다.