ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동적인 데이터를 처리하는 다양한 방법.
    간단기법 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 같은 라이브러리가 선호된 이유이기도 하다.

    댓글

Designed by Tistory.