본문 바로가기
Web/Typescript

effective Typescript (7)

by HanJunseo 2025. 3. 19.

매핑된 타입을 사용하여 값을 동기화하기

산점도를 그리기 위한 UI 컴포넌트를 작성한다고 해보자. 여기에는 디스플레이와 동작을 제어하기 위한 몇 가지 다른 타입의 속성이 포함된다.

interface ScatterProps {
// The data
xs: number[];
ys: number[];

// Display
xRange: [number, number];
yRange: [number, number];
color: string;

// Events
onClick: (x: number, y: number, index: number) => void;
}

불필요한 작업을 피하기 위해 필요할 때에만 차트를 다시 그릴 수 있다.
데이터나 디스플레이 속성이 변경되면 다시 그려야 하지만, 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다.

이런 종류의 최적화는 리액트 컴포넌트에서는 일반적인 일인데, 렌더링 할 때마다 이벤트 핸들러 Prop이 새 화살표 함수로 설정된다.

최적화를 두 가지 방법으로 구현해보자. 다음은 첫 번째 방법이다.

function shouldUpdate(
oldProps: ScatterProps,
newProps: ScatterProps
) {
    let k: keyof ScatterProps;

    for (k in oldProps) {
        if(oldProps[k] !== newProps[k]) {
        if(k !== 'onClick') return true;
        }
    }
    return false;
}

만약 새로운 속성이 들어오면 shouldUpdate 함수는 값이 변경될 때마다 차트를 다시 그릴 것이다.
이렇게 처리하는 것을 '보수적 접근법' 또는 '실패에 닫힌 접근법'이라고 한다.
이 접근법을 이용하면 차트가 정확하지만 너무 자주 그려질 가능성이 있다.

두 번째 최적화 방법은 다음과 같다. '실패에 열린 접근법'을 사용했다.

function shouldUpdate(
oldProps: ScatterProps,
newProps: ScatterProps
) {
    return (
        oldProps.xs !== newProps.xs ||
        oldProps.ys !== newProps.ys ||
        oldProps.xRange !== newProps.ys ||
        oldProps.yRange !== newProps.yRange ||
        oldProps.color !== newProps.color
        // (no check for onClick)
    );
}

이 코드는 차트를 불필요하게 다시 그리는 단점을 해결했다. 하지만 실제로 차트를 다시 그려야 할 경우에 누락되는 일이 생길 수 있다.
이는 히포크라테스 전집에 나오는 원칙 중 하나인 '우선, 망치지 말 것'을 어기기 때문에 일반적인 경우에 쓰이는 방법은 아니다.

앞선 두 가지 최적화 방법 모두 이상적이지 않다. 새로운 속성이 추가될 때 직접 shouldUpdate를 고치도록 하는 게 낫다. 이 내용을 주석으로 추가해 보자.

interface ScatterProps {
    xs: number[];
    ys: number[];
    // ...
    onClick: (x: number, y: number, index: number) => void;
    // 참고: 여기에 속성을 추가하려면, shouldUpdate를 고치세요!

}

그러나 이 방법 역시 최선이 아니며 타입 체커가 대신 할 수 있게 하는 것이 좋다.
다음은 타입 체커가 동작하도록 개선한 코드이다. 핵심은 매핑된 타입과 객체를 사용하는 것이다.

const REQUIRES_UPDATE: {[k in keyof ScatterProps]: boolean} = {
    xs: true,
    ys: true,
    xRange: true,
    yRange: true,
    color: true,
    onClick: false,
};



function shouldUpdate(
oldProps: ScatterProps,
newProps: ScatterProps
) {
    let k: keyof ScatterProps;
    for (k in oldProps) {
        if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
        return true;
        }
    }
    return false;
}

[k in keyof ScatterProps]은 타입 체커에게 REQUIRES_UPDATE가 ScatterProps과 동일한 속성을 가져야 한다는 정보를 제공한다. 나중에 ScatteProps에 새로운 속성을 추가하는 경우 다음 코드와 같은 형태가 될 것이다.

interface ScatterProps {
    // ...
    onDoubleClick: () => void;
}```

그리고 REQUIRES_UPDATE의 정의에 오류가 발생한다.

> property 'clock' is missing in type '{ xs: true; ys: true; xRange: true; yRange: true; color: true; onClick: false; }' but required in type '{ xs: boolean; ys: boolean; xRange: boolean; yRange: boolean; color: boolean; clock: boolean; onClick: boolean; }'

이런 방식은 오류를 정확히 잡아낸다. 속성을 삭제하거나 이름을 바꾸어도 비슷한 오류가 발생한다. 

여기서 boolean 값을 가진 객체를 사용했다는 점이 중요하다. 배열을 사용했다면 다음과 같은 코드가 된다.

```typescript
const PROPS_REQUIRING_UPDATE: (keyof ScatterProps)[] = [
    'xs',
    'ys',
    // ...
]

여기서 우리는 실패에 열린 방법을 선택할 지, 닫힌 방법을 선택할 지 정해야 한다.
매핑된 타입은 한 객체가 또 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적이다.

'Web > Typescript' 카테고리의 다른 글

effective Typescript (6)  (0) 2025.03.18
effective Typescript (5)  (0) 2025.03.17
effective Typescript (4)  (0) 2025.03.16
effective Typescript (3)  (0) 2025.03.14
effective Typescript (2)  (0) 2025.03.13