느릿늘있

[TypeScript] 포스텔의 법칙 In TypeScript (w. 이펙티브 타입스크립트) 본문

개발공부

[TypeScript] 포스텔의 법칙 In TypeScript (w. 이펙티브 타입스크립트)

JHKim93 2024. 12. 17. 22:42

    이번에는 이펙티브 타입스크립트 4장에 나오는 " 포스텔의 법칙 IN TypeScript"라는 주제를 제 나름의 방식대로 해석하고 정리한 내용을 공유해보려고 합니다! 본 내용은 항해 플러스 프론트엔드 3기 토요 지식회에 발표했던 내용을 기반으로 작성하였습니다!

  <객체 지향의 사실과 오해>라는 책을 읽으면서 개발이라는 게 그냥 기능만 구현하는 것 너머에 이런 철학적인 바탕이 있을 수 있구나라는 것을 알게 되었습니다. 그러던 와중에 <이펙티브 타입스크립트>를 읽게 되었고 책에 나오는 이 29번 Item이 함수형 프로그래밍에서 그러한 철학 중 하나라고 느껴졌고 이에 매력을 느껴 깊게 공부해보게 되었습니다.

  견고성의 원칙이라고도 불리는 포스텔의 법칙은 TCP 초기 모델을 구축한 Jon Postel이 남긴 말입니다. 자유도가 아주 높은 인터넷이라는 환경에서 견고한 통신 시스템을 구축하기 위해서는 받아들이는 네트워크(요청)에 대해서는 자유롭게 열어두고 보내는 네트워크(응답)에 있어서는 보수적으로 보내면 된다는 얘기입니다. 견고한 네트워크를 유지하기 위해서 보수적으로 요청하고 보수적으로 응답해야한다는 고전적인 법칙에서 벗어나 자유의 바다인 인터넷에서는 이러한 원칙을 지키는 것이 견고한 네트워크를 갖추는 방법이라는 것이죠.

  이를 함수의 영역으로 가져와보겠습니다. 함수형 프로그래밍이란 함수를 최대한 작은 단위로 쪼개고 이들을 이어 붙여서 비지니스 로직을 하나씩 구축해 나가는 방법론입니다. 이를 구현하기 위한 최적의 함수 형태는 위 이미지에서 몇 번이라고 생각하시나요?

  견고한 로직을 만드는 방법으로 2번과 4번 같은 형태도 나쁘지 않아 보입니다. 하지만 문제는 우리의 프로그램은 인터넷과 같이 방대하고 자유롭다는 점입니다. 시간이 지남에 따라 요구사항이 변하기도 하고 개발자가 바뀌기도 하죠. 이 때, 2번과 4번 같은 구조는 그 견고함을 오래 지속하기 어려워 보입니다.

  그렇다면 3번은 어떤가요? input이 넓고 output이 좁게 생긴 형태를 이어 붙였을 때 사이드 이펙트 없이 견고하게 동작할 것처럼 보입니다.

  그리고 이 형태가 바로 우리가 "함수"라고 부르는 것의 대표적인 심볼입니다.


  이러한 배경 지식을 바탕으로 타입스크립트를 활용해 어떻게 타입 설계를 하면 좋을 지 제 나름대로 정리한 포스텔의 법칙 In TypeScript 3가지 원칙을 소개해드리겠습니다.

1. 유니온 타입에서 대표 타입 정하기

  코드를 작성하다보면 특정 파라미터의 타입이 A일수도 있고 A`일 수도 있는 경우가 종종 있습니다. 예를 들면, 위치를 표현하기 위해 위도 경도를 변수로 사용할 때 아래와 같이 두 가지 형태로 받을 수 있겠죠.

{ lng : number, lat : number } | { lon : number, lat : number }

  이런 경우, 포스텔의 법칙을 적용하는 첫 번째 방법이 유니온 타입에서 대표 타입 정하기 입니다. 해당 객체의 타입이 여러 가지 방법으로 함수 내부에 들어올 수 있을 때, 함수 내부에서 사용하고 반환할 대표 타입을 하나 정해서 먼저 하나의 엄격한 타입으로 변환하는 과정을 거칩니다. 엄격한 타입으로 함수 내 모든 로직이 동작하도록 설계하고 해당 값을 반환해야 하는 경우에도 엄격한 타입으로 반환되도록 합니다.

// 유니온 타입 하나만 사용 [BAD]
type tLngLat = { lng: number; lat: number } | { lon: number; lat: number };

// 대표 타입을 지정하여 사용 [GOOD]
type tLngLat = { lng: number; lat: number }; // 엄격한 타입
type tLngLatLike = tLngLat | { lon: number; lat: number }; // 느슨한 타입

2. 엄격한 타입에서 느슨한 타입으로 확장하기

  타입스크립트의 유틸리티 타입에 익숙하지 않은 개발자들이 자주 하는 실수 중 하나가 느슨한 타입을 먼저 선언하고 엄격한 타입을 이를 확장해서 사용하는 것입니다.

// 느슨한 타입에서 엄격한 타입으로 확장 [BAD]
interface IOptionalCamera { center?: tLngLat; zoom?: number };
interface ICamera extends IOptionalCamera { center: tLngLat };

  이를 엄격한 타입에서 느슨한 타입으로 확장하기를 적용하여 포스텔 형님을 기쁘게 만들어 봅시다. 엄격한 타입에서 느슨한 타입으로 확장을 하기 위해서는 타입스크립트의 유틸리티 타입을 잘 활용해야 합니다.

// 엄격한 타입에서 느슨한 타입으로 확장 [GOOD]
// 엄격한 타입
interface ICamera {
  center: tLngLat;
  zoom?: number;
}

// 느슨한 타입
interface IOptionalCamera extends Omit<Partial<ICamera>, 'center'> {
  center?: tLngLat
}

  여기서 발생하는 아주 중요한 차이는 엄격한 타입에 영향을 주지 않고 느슨한 타입을 자유롭게 확장시킬 수 있다는 점입니다. 느슨한 타입을 확장해서 엄격한 타입을 선언하는 경우에는 느슨한 타입의 수정사항이 그대로 엄격한 타입에 영향을 주게 된다는 점을 생각한다면 왜 이 원칙을 지켜야하는 지 이해가 되실겁니다.

3. 매개변수는 너그럽게 반환값은 견고하게

  마지막 원칙은 위와 같이 타입 설계를 마친 후 이를 함수에 적용할 때 지켜주어야 하는 원칙입니다. 위와 같이 엄격한 타입과 느슨한 타입이 명료하게 구분되었다면, 함수의 매개변수에는 가능하면 느슨한 타입을 사용하고, 함수의 반환값과 내부 로직에는 가능하면 엄격한 타입을 적용시켜주면 됩니다.

// 함수에 적용
function focusOnFeature(feature: { position: tLngLatLike }) // 함수의 매개변수 타입은 너그럽게
  :{ position: tLngLat } // 함수의 반환 타입은 견고하게
  { ... }

// 훅에 적용
function useCamera(): {
  camera: ICamera; // 사용하는 반환값의 타입은 견고하게
  setCamera: (options: IOptionalCamera) => void; // 설정하는 함수의 매개변수는 너그럽게
}

  기능 구현의 관점에서 위와 같은 원칙을 지키지 않더라도 얼마든지 함수형 프로그램을 구현할 수 있습니다. 어쩌면 이러한 타입 설계를 고민하는 시간이 더 아까운 경우도 있을 수도 있겠습니다. 하지만 빠르게 가려고만 하기 보다는 이러한 사소한 원칙들을 잘 다듬고 쌓아나가는 것이 좋은 개발자로 오래 나아갈 수 있는 힘이 되어줄 것이라는 생각에 별 것 아닌 것 같은 이야기를 길게 풀어 보았습니다.

  이 긴 글을 여기까지 읽어 주신 분이 몇 분이나 계시겠냐마는 한 분이라도 계시다면 읽어주셔서 정말 감사드리고 오히려 남들이 잘 모르는 중요한 원칙을 생각할 수 있는 개발자가 되신 것을 축하드리며 내 맘대로 정리한 포스텔의 법칙 In TypeScript를 마무리 하도록 하겠습니다. 감사합니다.