본문 바로가기

IT

Effective Typescript - 31 - 타입 주변에 null 값 배치하기

strictNullChecks 설정을 활성화 시키면 오류를 회피하기위해 null check하는 예외 처리 구문이 코드 전반에 추가 되어야 한다고 생각 할 수 있다. 그러나, 값이 전부 null이거나 전부 null이 아닌 경우로 분명하게 구별될 수만 있다면 코드 작성이 훨씬 쉬워진다고 한다. 이와 관련하여 타입에 null을 추가하는 방식을 사용했을 때 어떻게 편리해 지는지 알아보도록 하자.

 

다음은 숫자들의 최소값과 최대값을 게산하는 extent 함수 예제이다.


// 이 함수의 문제
// nums가 깡통 배열이면 [undefined, undefined] 배열이 반환된다. 객체에 undefined 가 포함되는 것을 최대한 기피하는 것이좋다고 한다.
// 배열에 0이 들어 잇으면 의도하지 않게 앞쪽 if(!min) 조건에 성립하게 된다.
function extent(nums: number[]){
    let min, max;
    for (const num of nums){
        if(!min) {
            min = num;
            max = num;
        }
        else {
            min = Math.min(min, num);
            max = Math.max(max, num); // (오류) max가 null인 상태에 대해서는?
        }
    }

    return [min, max];
}

// 수정
function extentFix(nums: number[]) {
    let result: [number, number] | null = null; // 타입 주변에 null 값 배치하기.

    for(const num of nums) {
        if(!result){
            result = [num, num];
        } else {
            result = [Math.min(num, result[0]), Math.max(num, result[1])];
        }
    }
    return result;
}

null과 null이 아닌 값을 혼재하여 사용하면 클래스에서도 문제가 발생한다.

예를 들어 사용자와 그 사용자의 포럼 게시글을 나타내는 클래스가 있다고 가정해보자.

// init 함수 내 프로미스가 완료되지 않은 상태에서 user와 Post 상태는 null이게 된다.
// 이 시점에서 user와 posts는 총 4가지 상태를 가질 수 있다.
// 둘다 null이거나 모두 null이 아니거나, 둘중 하나만 null인 두가지 상태.
// 속성 값의 불확실성이 클래스의 모든 매서드에 악영향을 미칠수 있다.
// 그래서 null 체크가 난발하게 되고 코드를 읽기가 불편해 지며 버그가 양산될 가능성을 높힌다.
class UserPosts {
    user: UserInfo | null;
    posts: Post[] | null;

    constructor(){
        this.user = null;
        this.posts = null;
    }

    async init(userId: string) {
        return Promise.all([
            async ()=> this.user = await fetchUser(userId),
            async ()=> this.posts = await fetchPostUser(userId)
        ]);
    }

    getUser(){
        ///... ?
        return this.user;
    }
}

위와 같은 클래스 설계는 분명히 문제가 있으므로 아래의 예제와 같이 개선할 수 있다.

class UserPostsFix {
    user: UserInfo;
    posts: Post[];

    constructor(user: UserInfo, posts: Post[]){
        this.user = user;
        this.posts = posts;
    }

    static async init(userId: string) : Promise<UserPostsFix> {
        const [user, posts] = await Promise.all([
            fetchUser(userId),
            fetchPostsForUser(userId)
        ]);
        return new UserPostsFix(user, posts);
    }

    getUserName() {
        return this.user.name;
    }
}

 

* 한 값의 null 여부가 다른 값의 null 여부에 암시적으로 관련되도록 설계하는 것은 피해야한다.

* API 작성 시에는 반환 타입을 큰 객체로 만들고 반환 타입 전체가 null이거나 null이 아니게 만들어야 한다.

* 클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.