사라진 자동완성 목록, 어디로 갔을까?

사라진 자동완성 목록, 어디로 갔을까?

얼마 전, 동료 기획자분에게서 메시지를 하나 받았습니다. "OO 페이지에서 자동완성 목록이 안 뜨는 것 같아요. 한번 확인해주실 수 있나요?" 분명 며칠 전까지만 해도 잘 동작하던 기능이었는데, 갑자기 안된다니 당황스러웠죠.

화면에서는 안 보이는데, DOM에는 있다?

곧바로 해당 페이지에 들어가서 직접 테스트해봤습니다. 기획자분 말씀대로 입력창에 글자를 입력해도 자동완성 목록이 나타나지 않더군요. "데이터가 안 넘어오나? 아니면 API에 문제가 생겼나?" 여러 생각이 스쳐 지나갔습니다.

가장 먼저 개발자 도구를 열어 네트워크 탭을 확인했습니다. 다행히 데이터는 정상적으로 잘 받아오고 있었어요. 그렇다면 문제는 프론트엔드 쪽이라는 건데… 혹시나 해서 DOM 구조를 한번 살펴봤습니다. 그런데 재미있는 사실을 발견했습니다. 자동완성 목록을 구성하는 HTML 요소들이 DOM 트리에는 멀쩡히 생성되어 있는 겁니다. 단지 화면에 보이지만 않을 뿐이었죠.

조금 더 자세히 살펴보니, 특정 부모 요소의 overflow: hidden 스타일 때문에 목록의 일부가 잘려나가면서 아예 보이지 않게 된 것이었습니다. 사용자가 보기에는 기능이 고장 난 것처럼 느껴질 수밖에 없는 상황이었죠.

왜 목록이 잘려 보였을까

이런 종류의 UI 문제는 생각보다 흔하게 마주칠 수 있습니다. 특정 컴포넌트의 UI가 부모 컴포넌트가 설정한 CSS 스타일에 의해 의도치 않게 가려지거나 잘리는 경우죠.

제 경우, 문제의 원인은 명확했습니다.

  1. 자동완성 목록은 입력 필드(input) 컴포넌트의 자식 또는 형제 요소로 DOM 트리에 그려집니다.
  2. 그런데 이 입력 필드를 감싸고 있는 상위 어딘가의 컨테이너 요소에 overflow: hidden 스타일이 적용되어 있었습니다.
  3. 이로 인해, 해당 컨테이너의 경계를 벗어나는 자동완성 목록은 화면에 그려지지 못하고 잘려나갔던 겁니다.

z-index 문제도 간혹 비슷한 현상을 일으키지만, 이번에는 overflow가 확실한 원인이었습니다. 해당 overflow: hidden을 잠시 개발자 도구에서 visible로 바꿔보니, 숨어있던 자동완성 목록이 정상적으로 나타나는 것을 확인할 수 있었으니까요.

하지만 그렇다고 해서 무턱대고 overflow: hidden 스타일을 제거할 수는 없었습니다. 다른 레이아웃에 영향을 줄 수 있기 때문이죠. 결국, 자동완성 목록 UI 자체를 렌더링하는 방식을 바꿔야 한다는 결론에 이르렀습니다.

사실 이 문제를 겪으면서, 현재 자동완성 컴포넌트의 렌더링 구조 자체에 대해 다시 한번 생각해보게 되었습니다. 자동완성 목록이 입력 필드와 같은 DOM 레벨, 혹은 그 하위에 직접적으로 렌더링되는 방식은 부모 요소의 CSS 스타일에 너무 쉽게 영향을 받는다는 단점이 있었죠. 드롭다운 메뉴나 모달처럼 독립적으로 화면 최상단에 떠야 하는 UI 요소들은 애초에 이런 구조적 제약에서 자유로울 필요가 있습니다.

Portal?

이럴 때 유용하게 사용할 수 있는 방법 중 하나가 바로 'Portal'입니다. Portal을 사용하면 특정 UI 요소를 현재 컴포넌트의 DOM 계층 구조에서 분리해서, 전혀 다른 위치(보통 HTML 문서의 <body> 태그 바로 아래)에 렌더링할 수 있습니다.

쉽게 말해, 자동완성 목록 UI를 원래 있던 자리에서 "뿅!" 하고 body 태그 직속 자식으로 순간이동 시키는 거죠. 이렇게 하면 부모 요소의 overflowz-index 같은 CSS 제약으로부터 자유로워질 수 있습니다. 모달 대화상자나 화면 전체를 덮는 툴팁 등을 구현할 때 자주 사용하는 기법이기도 합니다.

코드로 보는 Portal 적용 (Angular CDK 예시)

제가 참여하고 있는 프로젝트는 Angular를 사용하고 있어서, Angular CDK(Component Dev Kit)에 있는 Overlay와 Portal 기능을 활용했습니다. React를 사용하신다면 ReactDOM.createPortal() 함수를 이용해 비슷하게 구현할 수 있습니다.

개념은 간단합니다.

  1. 먼저, 화면 최상단에 우리가 원하는 UI(자동완성 목록)를 띄울 수 있는 보이지 않는 컨테이너(OverlayRef)를 만듭니다.
  2. 그리고 우리가 보여주고 싶은 자동완성 목록 템플릿을 Portal(TemplatePortal)로 감싸줍니다.
  3. 마지막으로, 이 Portal을 앞서 만든 Overlay 컨테이너에 쏙 넣어주면 됩니다.
// 1. Overlay 서비스 주입 및 자동완성 패널 템플릿 참조 준비
// constructor(private overlay: Overlay, private viewContainerRef: ViewContainerRef) {}
// @ViewChild('autoCompletePanel') panelTemplate: TemplateRef<any>;

private openAutoCompletePanel() {
  // 2. Overlay 컨테이너 생성 (위치 전략 등 설정 가능)
  const overlayRef = this.overlay.create({
    // 필요에 따라 위치나 스크롤 전략을 설정합니다.
    // 예: positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
  });

  // 3. 자동완성 패널 템플릿을 TemplatePortal로 만듭니다.
  const templatePortal = new TemplatePortal(this.panelTemplate, this.viewContainerRef);

  // 4. Portal을 Overlay 컨테이너에 연결(attach)합니다.
  overlayRef.attach(templatePortal);

  // 필요시 overlayRef.detach() 또는 overlayRef.dispose()로 해제합니다.
}

이렇게 하면 자동완성 목록은 더 이상 원래 있던 DOM 위치의 CSS 제약을 받지 않고, 화면 최상단 레벨에서 자유롭게 표시될 수 있습니다. 덕분에 overflow: hidden 문제로부터 벗어나 목록 전체가 시원하게 나타나는 것을 확인할 수 있었습니다.

마무리

단순히 UI가 잘려 보이는 문제를 해결하기 위해 시작했지만, Portal을 적용하면서 UI 렌더링 방식에 대해 다시 한번 생각해 볼 수 있었습니다. 컴포넌트의 시각적 표현을 논리적 구조와 분리할 수 있다는 점은 UI 개발의 유연성을 크게 높여줍니다.

물론 Portal을 사용할 때는 몇 가지 더 신경 써야 할 부분이 있습니다.

  • 접근성: Portal로 띄워진 요소들도 키보드 네비게이션이나 스크린 리더 사용에 문제가 없도록 aria 속성 설정이나 포커스 관리에 주의해야 합니다. 예를 들어 모달이 열렸을 때 키보드 포커스가 모달 내부에만 머무르도록 하는 '포커스 트랩(focus trap)' 같은 처리가 필요할 수 있습니다.
  • 상태 관리: Portal 내부의 UI와 이를 호출한 원래 컴포넌트 간에 상태를 주고받거나 이벤트를 처리하는 방식을 명확히 해야 합니다.
  • 스타일링: 전역 CSS의 영향이나 Portal로 띄워진 요소에만 특정 스타일을 적용할 때, 스타일 충돌이나 적용 범위에 대해 주의 깊게 살펴봐야 합니다.

이런 점들을 잘 고려한다면, Portal은 복잡한 UI 레이아웃 문제나 부모 요소의 스타일 제약으로 인해 발생하는 여러 골치 아픈 상황들을 해결하는 데 도움이 될 겁니다. 혹시 비슷한 문제로 고민하고 계셨다면, Portal 도입을 한번 검토해 보시는 것도 좋겠습니다.