PUBLISHED

Loose Autocomplete

작성일: 2025.09.14

Loose Autocomplete

프로그래밍을 하다 보면 개발자가 의도한 선택지를 에디터가 자동완성으로 얼마나 잘 안내해 주느냐가 개발 경험에 큰 영향을 미친다는 사실을 자주 체감하게 된다. 특히 문자열 기반의 옵션을 다룰 때는 이 차이가 더욱 극명하게 드러난다.

예를 들어 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를 지켜낸다.

이를 일반화한 타입은 다음과 같이 정의할 수 있다.

untitled
TS
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"와 같은 커스텀 문자열을 입력해도 타입 오류는 발생하지 않는다. 프로젝트 차원에서 공식적으로 권장하는 메서드는 다섯 가지이지만, 예외적인 상황에서의 확장 가능성까지 함께 고려한 타입 설계라 할 수 있다.

untitled
TS
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와 유사한 접근이 필요하다.

untitled
TS
type Size =
  | "sm"
  | "md"
  | "lg"
  | ([number, number] & {})  // 커스텀 가로/세로 크기
  | (number & {});           // 커스텀 정사각형 크기

이렇게 정의하면 문자열 옵션은 자동완성으로 명확히 제안되고, 숫자나 배열을 통한 커스텀 입력도 자연스럽게 허용된다. 이를 사용하는 컴포넌트에서는 타입 가드를 통해 각 경우를 분기 처리할 수 있다.

untitled
TSX
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>
  );
}
untitled
JSX
<Component size="sm" />        // 자동완성 추천
<Component size={50} />        // 숫자 직접 지정
<Component size={[80, 120]} /> // 커스텀 가로/세로 크기

결론

이처럼 Loose Autocomplete는 타입스크립트에서 자동완성과 타입 안전성을 동시에 만족시키기 위한 실용적인 패턴이다. 기본 옵션은 에디터가 적극적으로 추천하도록 하면서, 프로젝트 특성상 필요한 예외적인 값도 배제하지 않는다. 결과적으로 개발자는 실수 가능성을 줄이면서도, 불필요하게 타입에 갇히지 않는 유연한 API를 사용할 수 있게 된다.

타입스크립트의 진가는 바로 이런 지점에서 드러난다. 단순히 “허용되느냐, 아니냐”를 판별하는 데 그치지 않고, 코드 작성 과정 자체를 더 나은 방향으로 유도하는 것. Loose Autocomplete는 그 철학을 잘 보여주는 작은 패턴 중 하나다.