티스토리 뷰

반응형

💣 개요

최근 프로젝트에서 화면 하단에 고정된 버튼을 구현하면서, 버튼의 위치가 동적으로 변경되지 않는 문제를 겪었습니다. 특히, 입력창(input)이 포커스될 때 버튼의 위치가 자동으로 조정되어야 했지만, 예상대로 동작하지 않아 사용자 경험에 문제가 발생했습니다.

1. 고정된 버튼의 위치 문제

화면 하단에 고정된 버튼이 position: fixed 속성으로 설정되어 있어, 사용자가 input을 포커스할 때, 버튼이 키보드에 의해 가려지거나 위치가 적절히 변경되지 않아 사용성이 떨어졌습니다.

2. 기존 해결 방법의 한계

CSS만으로 문제를 해결하려 했으나, 다양한 화면 크기와 상황을 고려했을 때 고정된 bottom 값으로는 모든 상황에 적절히 대응할 수 없었습니다.

이 문제를 해결하기 위해 React 포털을 도입하여 버튼을 DOM 트리 외부의 특정 위치에 독립적으로 렌더링함으로써, 유동적으로 위치를 조정할 수 있는 솔루션을 구현하게 되었습니다.

이번 포스트에서React 포털을 사용하여 버튼 위치 설정을 해결한 과정을 설명합니다.

💡 리액트 포털(React Portal)이란?

React 포털은 컴포넌트를 일반적인 React 컴포넌트 트리 외부의 DOM 노드에 렌더링합니다. 이 기능을 사용하면 특정 UI 요소를 별도의 DOM 위치에 렌더링할 수 있습니다. 예를 들어, 모달 창을 루트 노드 아래에 직접 렌더링할 수 있습니다.

React 포털(Portal)은 UI 요소를 현재 컴포넌트 트리와 독립적으로 특정 DOM 노드에 렌더링할 수 있는 강력한 기능입니다. 버튼과 같은 요소를 구현할 때 리액트 포털은 중첩된 컴포넌트 구조에서 발생할 수 있는 스타일 충돌 문제를 해결해 줍니다. 리액트 포털 컴포넌트는 이런 상황에서 모든 요소를 통합하고 관리하는 역할을 합니다. 이를 통해 모달, 알림, 툴팁 같은 UI 요소를 더욱 유연하게 배치할 수 있습니다.

🚦리액트 포털의 장점

1. 레이어 관리 및 스타일 격리

모달, 툴팁, 드롭다운과 같은 UI 컴포넌트는 종종 페이지의 다른 요소들 위에 떠 있어야 하며, 포커스나 클릭 이벤트에 의해 쉽게 닫혀야 합니다. 포털을 사용하면 이러한 요소들이 body나 다른 상위 DOM 노드에 렌더링되어 CSS 겹침 문제, z-index 문제를 쉽게 관리할 수 있습니다. 스타일 격리를 통해 복잡한 레이아웃에서 원하는 스타일을 정확하게 적용할 수 있습니다.

2. DOM 구조와 무관한 렌더링

포털을 사용하면 React 컴포넌트 트리와 DOM 트리가 일치하지 않아도 됩니다. 이는 컴포넌트가 렌더링되는 위치에 대한 더 많은 유연성을 제공합니다. 예를 들어, React 컴포넌트는 특정 부모 요소의 하위에 있지만, 렌더링된 결과는 body 아래에 위치하게 할 수 있습니다. 이는 모달이나 알림 같은 요소가 루트 요소와 직접적으로 상호작용하지 않고 독립적으로 동작할 수 있도록 합니다.

3. 레이어 및 이벤트 버블링 제어

포털을 사용하면 이벤트 버블링을 원하는 대로 제어할 수 있습니다. 예를 들어, 모달 내의 클릭이 부모 요소로 버블링되지 않게 함으로써 모달 외부를 클릭했을 때 모달을 닫는 것과 같은 상호작용을 쉽게 구현할 수 있습니다. 특정 이벤트가 페이지 구조와 독립적으로 작동하도록 하여 복잡한 상호작용을 더 쉽게 처리할 수 있습니다.

4. 화면 리프레시 및 스크롤링 문제 해결

페이지 스크롤링이 있을 때, 포털을 사용하여 고정된 위치에 모달이나 툴팁을 유지할 수 있습니다. 이는 페이지 콘텐츠가 움직여도 모달이나 팝업이 원하는 위치에 남아있게 합니다. 스크롤과 관련된 이슈를 해결하고, 원하는 위치에 고정된 UI를 제공하는데 유용합니다.

5. React의 컴포넌트 구조와 독립적인 UI 렌더링

특정 컴포넌트가 복잡한 구조나 스타일을 요구할 때, 트리 외부에 렌더링함으로써 DOM 구조에 영향을 덜 주면서 독립적으로 스타일링 및 렌더링할 수 있습니다.

💡 리액트 포털을 사용한 버튼 구현

1. GlobalPortal 컴포넌트 정의

우리는 포털의 기능을 쉽게 사용할 수 있도록 GlobalPortal 컴포넌트를 정의합니다. 이 컴포넌트는 Provider와 Consumer로 나누어져 있습니다.

GlobalPortal.Provider: 애플리케이션의 최상위에서 포털 컨텍스트를 제공합니다.
GlobalPortal.Consumer: 컨텍스트에서 제공하는 포털 컨테이너에 자식 요소를 렌더링합니다.

// GlobalPortal.tsx

import { ReactNode, createContext, useRef } from 'react';
import { createPortal } from 'react-dom';

// 포털 컨텍스트 생성, 기본값은 null
const PortalContext = createContext<HTMLElement | null>(null);

const PortalProvider = ({ children }: PortalProviderProps) => {
  // 포털 루트를 참조하는 useRef, 초기에는 null
  const portalRootRef = useRef<HTMLDivElement | null>(null);

  // 처음 렌더링 시 포털 루트를 생성하여 body에 추가
  if (!portalRootRef.current) {
    ...
  }

  return (
    <PortalContext.Provider value={portalRootRef.current}>
      {children}
    </PortalContext.Provider>
  );
};

interface PortalRendererProps {
  children: ReactNode;
}

const PortalRenderer = ({ children }: PortalRendererProps) => (
  <PortalContext.Consumer>
    {portalRoot => portalRoot ? createPortal(children, portalRoot) : null}
  </PortalContext.Consumer>
);

export const GlobalPortal = {
  Provider: PortalProvider,
  Renderer: PortalRenderer,
};

 

2. GlobalPortal.Provider를 App 최상단에 배치하기

GlobalPortal 컴포넌트는 포털을 통해 특정 UI 요소를 DOM 트리의 다른 위치에 렌더링하는 용도로 사용됩니다. 이를 고려하여, GlobalPortal.Provider를 최상위 레벨에 배치하여 애플리케이션 전체에서 포털을 사용할 수 있도록 설정하는 것이 좋습니다. 일반적으로 App 컴포넌트의 루트 근처에 배치합니다.

// App.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Route, Routes } from "react-router-dom";
import { lazy, Suspense } from "react";
import AppLayout from "../layout/AppLayout";
import { GlobalPortal } from "@/components/shared/GlobalPortal";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <GlobalPortal.Provider>
        <Routes>
            <Route path="/" element={<AppLayout />}>
              <Route index element={<HomePage />} />
             
             ...
             
             
            </Route>
          </Routes>
      </GlobalPortal.Provider>
    </QueryClientProvider>
  );
}

export default App;

 

3. 포털을 사용한 버튼 구현

이제 GlobalPortal.Consumer를 사용하여 포털을 통해 버튼을 렌더링해 보겠습니다. 이 버튼은 React 컴포넌트 트리의 구조와 관계없이, 포털을 통해 지정된 DOM 위치에 렌더링됩니다. 포털로 렌더링된 요소도 일반적인 React 이벤트 시스템의 혜택을 그대로 받습니다.

//Button.tsx

const Button = ({ className, title, onClick, disabled }: ButtonProps) => {
  return (
    <GlobalPortal.Consumer>
      <button className={cx(["container", className])} onClick={onClick} disabled={disabled}>
        {title}
      </button>
    </GlobalPortal.Consumer>
  );
};
export default Button;

🙌 결과

이제 input 포커스 시 키보드가 올라와도 버튼이 가려지지 않고 자동으로 위치가 조정됩니다. React 포털을 사용하여 버튼을 DOM 트리 외부의 별도 컨테이너에 렌더링함으로써 다른 UI 요소들과 겹치지 않도록 조정되어 사용자 경험이 크게 향상되었습니다. 개발자 도구에서 dom구조에 대해 확인해보면 포탈 컨테이너가 #root 내부의 다른 요소들과 같은 수준에 위치하고 있습니다. 이는 포탈이 DOM 트리의 원하는 위치에 적절하게 추가되었음을 의미합니다. 

⭐️ 요약

포털을 사용하여 DOM 트리 외부에 UI 요소를 렌더링하면, 스타일 격리, 이벤트 관리, 레이아웃 관리, 그리고 사용자 경험 개선 등에서 많은 이점을 제공합니다.  왜냐하면 리액트 포털을 사용하면 해당 컴포넌트가 애플리케이션의 다른 요소와 겹치지 않고, 독립적으로 스타일링과 상태 관리가 가능하기 때문입니다. 이러한 요소들은 독립적으로 작동하면서도 사용자가 필요로 하는 방식으로 상호작용할 수 있도록 설계할 수 있게 해줍니다.

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함