etc

복잡한 프로젝트 환경에서 DeepReadonly 타입을 활용한 불변성 강화

하리하링웹 2024. 2. 26. 19:24

 현재 다니고있는 회사에서는 약 100명 정도의 개발자가 단일 저장소에서 코딩을 진행하고 있다. 이러한 복잡한 작업 환경에서는 개발자에 의해 발생할 수 있는 실수나 예측할 수 없는 에러를 방지하는 것이 매우 중요하다. 이에 따라, 객체의 모든 레벨에서 객체의 변경을 방지할 수 있는 방안이 필요했고, TypeScript를 활용하여 DeepReadonly 유틸리티 타입을 만들게 되었다.

 

DeepReadonly 타입은 객체의 모든 프로퍼티를 재귀적으로 읽기 전용으로 만들어, 객체의 불변성을 보장해준다. 프로젝트의 규모가 커질수록 코드의 안정성을 유지하는 것이 중요하며, DeepReadonly 타입은 객체의 중첩된 구조까지 보호함으로써 개발자들이 실수로 객체를 변경하는 것을 방지할 수 있다.

구현 코드

type DeepReadonly<T> = T extends OurFunction
    ? T
    : T extends Record<string, any>
    ? {
        readonly [K in keyof T]: DeepReadonly<T[K]>;
      }
    : T;

여기서, 함수는 자바스크립트에서 1급 객체로 취급되지만 재귀 구문을 실행하면 안 되므로 별도의 타입 OurFunction을 정의하여 필터링하였다.

 

type OurFunction = (...args: any[]) => any;

 

작동 원리

1. T extends OurFunction ? T : T 를 통해 함수 타입은 재귀적으로 처리하지 않고 그대로 반환한다.

2. T extends Record<string, any> ? { readonly [K in keyof T]: DeepReadonly<T[K]>; } : T  는 객체 타입인 경우, 객체의 각 속성 K에 대해 DeepReadonly를 재귀적으로 적용하여 객체의 중첩된 속성까지 readonly로 만들어주는 역할을 해준다.

 

후기

이러한 방식으로 객체의 불변성을 보장하면서, 개발자가 실수로 핵심 객체를 변경하는 것을 타입적으로 방지할 수 있었다.

또한, Writable한 객체가 필요한 상황에 대비하여 클래스 내부에 getWritable 메서드를 추가하고, 이를 통해 deepClone된 객체를 반환하게 했다. 이후 객체는 set 메서드를 통해서만 업데이트할 수 있도록 구현했으며, 이는 redux가 dispatch를 통해서만 store를 업데이트하게 하는 방식과 유사하게 동작하고 사용하기를 바라면서 개발하였다. (immer와 같은 기능도 추가 개발했으나, 이는 별개의 개념이므로 여기서는 생략합니다.)