
프로그래밍을 하다 보면 개발자가 의도한 선택지를 에디터가 자동완성으로 얼마나 잘 안내해 주느냐가 개발 경험에 큰 영향을 미친다는 사실을 자주 체감하게 된다. 특히 문자열 기반의 옵션을 다룰 때는 이 차이가 더욱 극명하게 드러난다.
예를 들어 HTTP 메서드를 표현할 때 "GET" | "POST" | "PUT" | "DELETE" | "PATCH"와 같은 리터럴 유니온 타입을 정의해두면, 에디터는 정확히 다섯 가지 선택지를 자동완성으로 제안해준다. 이 덕분에 개발자는 가능한 옵션을 한눈에 파악할 수 있고, 잘못된 문자열을 입력했을 때 즉시 타입 오류를 통해 피드백을 받는다.
이 방식은 단순한 편의성을 넘어, 실수를 사전에 차단해 주는 강력한 안전장치로 작동한다. 허용된 값의 집합이 타입 수준에서 명확하게 드러나기 때문에, 코드 작성 과정 자체가 자연스럽게 가이드 역할을 하게 된다. 타입스크립트가 제공하는 자동완성과 타입 체크가 이상적으로 맞물려 동작하는 대표적인 사례라 할 수 있다.
하지만 문제는 현실의 요구사항이 항상 이렇게 단순하지 않다는 점이다. 실제 프로젝트에서는 표준 HTTP 메서드만을 다루는 경우보다, 상황에 따라 특수한 메서드나 확장 문자열을 허용해야 하는 경우도 적지 않다. 이때 "CONNECT", "TRACE", "OPTIONS"처럼 거의 사용하지 않는 메서드까지 자동완성 목록에 함께 노출된다면, 오히려 선택지가 과도하게 늘어나 개발 경험이 나빠질 수 있다. 반대로, 이를 피하기 위해 string 전체를 허용해버리면 또 다른 문제가 발생한다.
예를 들어 타입을 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | string과 같이 정의하면, 타입 시스템 관점에서는 모든 문자열이 허용되므로 문제가 없어 보인다. 그러나 에디터 입장에서는 이 타입을 사실상 string으로 인식하게 되고, 결과적으로 다섯 가지 주요 옵션에 대한 자동완성은 완전히 사라진다. 즉, 유연성을 얻기 위해 자동완성을 포기하게 되는 셈이다. 이는 우리가 기대하는 방향과는 정반대의 결과다.
Loose Autocomplete 패턴의 구현
이상적인 형태는 분명하다. “자주 쓰는 주요 옵션 몇 개는 자동완성으로 명확히 추천하되, 필요하다면 그 외의 문자열도 입력할 수 있는” 타입이 필요하다. 이처럼 자동완성과 유연성을 동시에 만족시키는 패턴을 흔히 Loose Autocomplete라고 부른다.
Loose Autocomplete 패턴의 핵심은 타입스크립트가 유니온 타입을 어떻게 해석하고, 언제 자동완성을 포기하는지를 역으로 활용하는 데 있다. 이 패턴을 구현할 때 자주 사용되는 테크닉이 바로 & {}를 활용하는 방식이다. 이는 타입스크립트가 타입을 한 번 더 “풀어서” 평가하도록 유도하는 일종의 트릭에 가깝다.
이전에 소개한 유틸리티 타입 Roll 역시 이와 유사한 아이디어를 기반으로 한다. 교차 타입이나 매핑된 타입을 IDE에서 더 읽기 좋은 형태로 정규화해 보여주기 위해, 의도적으로 빈 객체 타입과 교차시키는 방식이다. Loose Autocomplete 역시 같은 원리를 활용해, 타입 안전성을 유지하면서도 자동완성 UX를 지켜낸다.
이를 일반화한 타입은 다음과 같이 정의할 수 있다.
type LooseAutocomplete<T> =
T extends string ? T | (string & {}) :
T extends number ? T | (number & {}) :
never;여기서 핵심은 단순히 string을 추가하는 것이 아니라, (string & {}) 형태로 교차시킨다는 점이다. 이렇게 하면 타입스크립트는 여전히 T로 주어진 리터럴 유니온을 “의미 있는 선택지”로 인식하여 자동완성에 노출한다. 동시에 타입 수준에서는 그 외의 임의 문자열도 허용되므로, 확장성 역시 확보된다.
HTTP 메서드 예시
이제 이 패턴을 HTTP 메서드에 적용해보자. 아래와 같이 Loose Autocomplete을 적용하면 에디터는 "GET", "POST", "PUT", "DELETE", "PATCH" 다섯 가지를 자동완성으로 제안한다. 그러나 필요하다면 "FOOBAR"와 같은 커스텀 문자열을 입력해도 타입 오류는 발생하지 않는다. 프로젝트 차원에서 공식적으로 권장하는 메서드는 다섯 가지이지만, 예외적인 상황에서의 확장 가능성까지 함께 고려한 타입 설계라 할 수 있다.
type HttpMethod = LooseAutocomplete<"GET" | "POST" | "PUT" | "DELETE" | "PATCH">;
만약 이 타입을 단순히 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | string으로 정의했다면, 에디터는 처음부터 자동완성을 포기했을 것이다. 이 미묘하지만 결정적인 차이가 바로 Loose Autocomplete 패턴의 가치다.
컴포넌트에서의 예시
이 패턴은 HTTP 메서드에만 국한되지 않는다. 프론트엔드 컴포넌트를 설계할 때도 매우 유용하게 활용할 수 있다. 예를 들어 size와 같이 자주 쓰이는 몇 가지 옵션과, 상황에 따라 완전히 커스텀한 값이 공존하는 경우를 생각해보자.
이미지나 박스 컴포넌트에서 "sm" | "md" | "lg" 같은 문자열 옵션은 자주 사용되지만, 때로는 숫자 하나로 정사각형 크기를 지정하거나, [width, height] 형태의 배열로 가로·세로 크기를 직접 제어하고 싶을 수도 있다. 이 모든 경우를 고려하면서도 자동완성을 포기하지 않으려면 Loose Autocomplete와 유사한 접근이 필요하다.
type Size =
| "sm"
| "md"
| "lg"
| ([number, number] & {}) // 커스텀 가로/세로 크기
| (number & {}); // 커스텀 정사각형 크기이렇게 정의하면 문자열 옵션은 자동완성으로 명확히 제안되고, 숫자나 배열을 통한 커스텀 입력도 자연스럽게 허용된다. 이를 사용하는 컴포넌트에서는 타입 가드를 통해 각 경우를 분기 처리할 수 있다.
interface ComponentProps {
size: Size;
}
function Component({ size }: ComponentProps) {
const boxSize =
typeof size === "string"
? getBoxSizeByText(size)
: Array.isArray(size)
? size
: [size, size];
return (
<div style={{ width: boxSize[0], height: boxSize[1] }}>
{/* 박스 콘텐츠 */}
</div>
);
}<Component size="sm" /> // 자동완성 추천
<Component size={50} /> // 숫자 직접 지정
<Component size={[80, 120]} /> // 커스텀 가로/세로 크기결론
이처럼 Loose Autocomplete는 타입스크립트에서 자동완성과 타입 안전성을 동시에 만족시키기 위한 실용적인 패턴이다. 기본 옵션은 에디터가 적극적으로 추천하도록 하면서, 프로젝트 특성상 필요한 예외적인 값도 배제하지 않는다. 결과적으로 개발자는 실수 가능성을 줄이면서도, 불필요하게 타입에 갇히지 않는 유연한 API를 사용할 수 있게 된다.
타입스크립트의 진가는 바로 이런 지점에서 드러난다. 단순히 “허용되느냐, 아니냐”를 판별하는 데 그치지 않고, 코드 작성 과정 자체를 더 나은 방향으로 유도하는 것. Loose Autocomplete는 그 철학을 잘 보여주는 작은 패턴 중 하나다.
더 읽어보기
2025.04.25
더 좁은 타입의 유효성에 대하여
사람이 무언가를 집중해서 바라보다 보면, 어느샌가 주변부가 흐려지고 가끔은 집중하고 있던 그 대상조차 보이지 않게 된다. 처음엔 분명하게 인식되던 경계가 서서히 사라지고, 오히려 애써 무시했던 주변이 본질을 가릴 때도 있다. 잘 보려 애쓰는 행위가, 역설적으로 시야를 좁히는 순간이다.개…
2025.04.06
재귀 조건부 타입에서의 추론 컨텍스트 손실
타입스크립트에서는 조건부 타입의 분배 과정에서도 타입 추론 컨텍스트가 유지된다. 이 특성 덕분에 단순한 분기 수준을 넘어, 상당히 복잡한 조건부 타입에서도 개발자가 의도한 방향으로 타입 추론을 유도할 수 있다. 실제로 이러한 특성은 고급 유틸리티 타입을 설계할 때 매우 강력한 도구로 작…
2025.03.28
유틸리티 타입 IsInRange
프론트엔드 개발을 하다 보면 컴포넌트의 props로 number 타입을 받을 때가 많다. 하지만 단순히 number 타입만 지정하면 값의 범위를 제한할 수 없다는 점이 아쉽다. 예를 들어, 페이지네이션의 currentPage는 1 이상의 값만 허용해야 하고, 슬라이더의 value는 최소…
2025.03.11
Equal but not Equal
TypeScript를 사용하다 보면, 겉보기에는 동일해 보이지만 실제로는 다르게 평가되는 타입을 마주치는 경우가 있다. 특히 객체 타입과 인터섹션 타입을 함께 다룰 때 이런 차이가 분명하게 드러난다.다음 두 타입을 살펴보자.type A = { x: string; y: string };…
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…
댓글
댓글을 불러오는 중...