
아래는 전역 상태 관리 라이브러리 Caro-kann의 persist 미들웨어 내부에서 사용되는 유틸리티 함수들의 타입 정의이다. 세 타입 모두 storageKey와 storageType이라는 동일한 파라미터를 요구하고 있으며, 이 둘은 저장소의 위치와 종류를 식별하기 위한 핵심 정보라는 점에서 공통된 맥락을 가진다.
type GetStorage = <T>(props: {
storageKey: string;
storageType: keyof StorageConfig | null;
migrate?: Migrate<T>;
initState: T;
}) => { state: T; version: number };
type SetStorage = <T>(props: {
storageKey: string;
storageType: keyof StorageConfig | null;
storageVersion: number;
value: T;
}) => void;
type ExecMigration = <T>(props: {
storageKey: string;
storageType: keyof StorageConfig | null;
migrate?: Migrate<T>;
}) => void;GetStorage와 SetStorage는 이름만 보더라도 비교적 직관적이다. 하나는 저장소로부터 값을 읽어오고, 다른 하나는 값을 저장소에 기록한다는 역할이 자연스럽게 연상된다. 반면 ExecMigration은 그렇지 않다. 이 타입이 정확히 어떤 책임을 가지는지, 왜 존재하는지, 그리고 앞선 두 타입과 어떤 관계를 맺고 있는지 한눈에 파악하기 어렵다.
특히 세 타입이 동일한 파라미터 구조를 공유하고 있음에도 불구하고, 이 사실은 타입 선언만 놓고 보면 우연처럼 보인다. 처음 코드를 접한 개발자라면 ExecMigration이 persist 미들웨어의 일부인지조차 확신하기 어렵고, GetStorage, SetStorage와는 별개의 독립적인 유틸리티라고 오해할 가능성도 충분하다. 즉, 타입 자체는 안전하지만, 타입이 전달하는 맥락과 의도는 충분히 드러나지 않는다.
이 지점에서 객체 지향 프로그래밍(OOP)의 사고방식이 하나의 힌트를 제공한다. OOP에서는 데이터를 중심으로 객체를 모델링하고, 그 객체가 가질 수 있는 행위들을 메서드로 함께 묶는다. 객체의 내부 구조와 그에 속한 동작들이 하나의 응집된 단위로 표현되기 때문에, 개발자는 “이 객체가 무엇을 책임지는지”를 자연스럽게 이해할 수 있다.
물론 TypeScript의 타입 설계가 OOP와 완전히 동일하다고 할 수는 없다. 하지만 “서로 밀접하게 연관된 데이터와 동작을 하나의 개념으로 묶어낸다”는 아이디어 자체는 충분히 차용할 수 있다. 이를 타입 수준에서 구현하는 한 가지 방법이 바로 인덱싱 타입과 객체 타입을 활용한 구조화이다.
export type PersistUtils = {
common: {
storageKey: string;
storageType: keyof StorageConfig | null;
};
getStorage: <T>(props: PersistUtils["common"] & {
migrate?: Migrate<T>;
initState: T;
}) => { state: T; version: number };
setStorage: <T>(props: PersistUtils["common"] & {
storageVersion: number;
value: T;
}) => void;
execMigration: <T>(props: PersistUtils["common"] & {
migrate?: Migrate<T>;
}) => void;
};이 구조에서는 세 함수가 공통으로 사용하는 파라미터가 common이라는 명시적인 이름 아래에 모인다. 그 결과, 중복을 제거했다는 기술적인 이점뿐 아니라, 이 파라미터들이 “persist 유틸리티 전반에서 공유되는 맥락”이라는 사실이 타입 자체로 드러난다. 또한 getStorage, setStorage, execMigration이 각각 독립적인 유틸리티가 아니라, 하나의 persist 도메인에 속한 역할이라는 점 역시 훨씬 분명해진다.
이러한 구조화의 효과는 타입 선언에만 국한되지 않는다. 실제 구현부에서도 동일한 이점을 얻을 수 있다.
export const getStorage: PersistUtils["getStorage"];
export const setStorage: PersistUtils["setStorage"];
export const execMigrate: PersistUtils["execMigration"];별도의 주석이나 설명이 없어도, 이 함수들이 어디에 속해 있고 어떤 맥락에서 사용되는지 타입 시그니처만으로 충분히 전달된다. 새로운 개발자는 구현을 열어보지 않더라도, 이 함수들이 persist 미들웨어를 구성하는 유틸리티 집합의 일부라는 사실을 자연스럽게 이해할 수 있다.
복잡한 타입 구조를 하나의 객체 타입으로 묶어 체계적으로 정리하는 행위는 단순히 “타입스크립트 문법을 잘 쓰는 것”과는 다른 차원의 문제다. 이는 코드의 응집도를 높이고, 암묵적인 규칙을 명시적인 구조로 끌어올리며, 결과적으로 협업과 유지 보수를 훨씬 수월하게 만든다. 코드는 더 이상 기계만을 위한 산출물이 아니라, 팀 내 다른 개발자와의 지속적인 커뮤니케이션 수단이 된다.
타입스크립트에서 타입 안전성을 보장하는 것은 분명 중요하다. 그러나 그것은 필요조건일 뿐, 충분조건은 아니다. 타입스크립트의 본질적인 가치는 단순한 타입 검사에 머무르지 않는다. 코드가 스스로 의도를 설명하고, 맥락을 드러내며, 개발자 간의 이해를 돕도록 설계할 수 있다는 점에 있다. 나는 타입 설계가 바로 그 지점까지 도달했을 때, 비로소 “좋은 타입스크립트 코드”라고 부를 수 있다고 믿는다.
더 읽어보기
2025.09.14
Loose Autocomplete
프로그래밍을 하다 보면 개발자가 의도한 선택지를 에디터가 자동완성으로 얼마나 잘 안내해 주느냐가 개발 경험에 큰 영향을 미친다는 사실을 자주 체감하게 된다. 특히 문자열 기반의 옵션을 다룰 때는 이 차이가 더욱 극명하게 드러난다.예를 들어 HTTP 메서드를 표현할 때 "GET" | "P…
2025.04.25
더 좁은 타입의 유효성에 대하여
사람이 무언가를 집중해서 바라보다 보면, 어느샌가 주변부가 흐려지고 가끔은 집중하고 있던 그 대상조차 보이지 않게 된다. 처음엔 분명하게 인식되던 경계가 서서히 사라지고, 오히려 애써 무시했던 주변이 본질을 가릴 때도 있다. 잘 보려 애쓰는 행위가, 역설적으로 시야를 좁히는 순간이다.개…
2025.04.06
재귀 조건부 타입에서의 추론 컨텍스트 손실
타입스크립트에서는 조건부 타입의 분배 과정에서도 타입 추론 컨텍스트가 유지된다. 이 특성 덕분에 단순한 분기 수준을 넘어, 상당히 복잡한 조건부 타입에서도 개발자가 의도한 방향으로 타입 추론을 유도할 수 있다. 실제로 이러한 특성은 고급 유틸리티 타입을 설계할 때 매우 강력한 도구로 작…
2025.03.28
유틸리티 타입 IsInRange
프론트엔드 개발을 하다 보면 컴포넌트의 props로 number 타입을 받을 때가 많다. 하지만 단순히 number 타입만 지정하면 값의 범위를 제한할 수 없다는 점이 아쉽다. 예를 들어, 페이지네이션의 currentPage는 1 이상의 값만 허용해야 하고, 슬라이더의 value는 최소…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
댓글
댓글을 불러오는 중...