searchgithubemail

React Parallel Routes

AOS 웹뷰에서 뒤로가기시 모달을 닫으려면?

2025-04-14

React Parallel Routes와 View Transitions API

웹뷰 개발시 AOS에서 뒤로가기 버튼을 누르면 모달이 닫히지 않고, 앱이 종료되거나 웹뷰가 닫히는 이슈가 생길 수 있다. 보통 모달을 만들 때 열고 닫히는 상태 기반으로 모달을 만든다. 이런 경우 애니메이션 처리나 URL 동기화, 뒤로가기 동작을 처리하기가 불편할 수 있다.

이러한 단점을 해결하기위해 react-router-dom으로 병렬 라우팅을 구현할 수 있다. 이 글에서는 react-router-dom을 사용하여 병렬 라우팅을 구현하는 방법과 View Transitions API를 활용하여 모달 애니메이션을 처리하는 방법에 대해 작성하고자 한다.

Nextjs 에서는 appRouter에서는 기본적으로 병렬 라우팅을 지원한다.

app/
├── layout.tsx
├── page.tsx              # 홈
├── inbox/
│   ├── page.tsx          # 리스트
│   ├── @modal/           # 병렬 슬롯
│   │   ├── default.tsx   # 모달 없는 기본 화면
│   │   └── [id]/page.tsx # 특정 모달 열릴 때
export default function InboxLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {modal}
    </div>
  );
}

대충 이런식으로 구현이 가능하다. 그럼 React에서는 어떻게 구현할 수 있을까?

먼저 간단하게 라우팅을 구성해보자

// App.tsx
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import ListPage from "./routes/ListPage.tsx";
import DetailModal from "./components/DetailModal.tsx";

const router = createBrowserRouter([
    {
        path: '/list',
        element: <ListPage />,
        children: [
            {
                path: ':id',
                element: <DetailModal />,
            },
        ],
    },
]);

function App() {
    return <RouterProvider router={router} />;
}


export default App

/list/1 로 접근했을 때, createPortal로 구성한 모달이 열리게하고, 뒤로가기 버튼을 누르면 모달이 닫히게 구현할 수 있다.

// ListPage.tsx
import {Outlet} from 'react-router-dom';
import useViewTransitionNavigate from "../hooks/useViewTransitionnavigate.ts";

export default function ListPage() {
    const navigate = useViewTransitionNavigate();

    return (
        <div>
            <h2>리스트 페이지</h2>
            <ul>
                {[1, 2, 3].map(id => (
                    <li key={id}>
                        <button onClick={() => navigate(`/list/${id}`)}>Item {id} 보기</button>
                    </li>
                ))}
            </ul>

            <Outlet />
        </div>
    );
}

ListPage에서는 Outlet을 사용하여 병렬 라우팅을 구현했다. 여기서 Outlet은 중첩 라우트를 렌더링하는 컴포넌트인데, 자식 컴포넌트를 렌더링하는 위치를 나타낼 수 있다. <Outlet />을 사용하여 ListPage의 자식인 모달을 렌더링할 수 있다.

    children: [
        {
            path: ':id',
            element: <DetailModal />,
        },
    ]
// DetailModal.tsx
import {useParams} from 'react-router-dom';
import Modal from './Modal';
import useViewTransitionNavigate from "../hooks/useViewTransitionnavigate.ts";

export default function DetailModal() {
    const { id } = useParams();
    const navigate = useViewTransitionNavigate();

    return (
        <Modal onClose={() => navigate('/list')}>
            <h3>Item {id} 상세</h3>
            <p>이건 모달입니다</p>
        </Modal>
    );
}
// useViewTransitionNavigate.tsx
import {useNavigate} from 'react-router-dom';

export default function useViewTransitionNavigate() {
    const navigate = useNavigate();

    return (to: string | number) => {
        if (typeof to === 'number') {
            return navigate(to);
        }

        if ('startViewTransition' in document) {
            (document).startViewTransition(() => navigate(to));
        } else {
            navigate(to);
        }
    };
}

간단한 hook을 만들어서 startViewTransition이 지원되는 경우에만 애니메이션을 적용하도록 했다. View Transitions API는 DOM 변경을 애니메이션으로 처리할 수 있는 API로, 브라우저에서 지원하는 경우에만 사용할 수 있다. 대부분 크롬계열의 브라우저만 지원하고, 아직 지원하지 않는 브라우저가 많아서 모바일 환경의 웹뷰에서는 사용하기 힘들것 같다.

View Transitions API 대해서는 다음에 작성하는걸로하고,, 위처럼 구현해서 모달을 띄우면 URL이 /list/1로 변경되고, /list 페이지에서 모달이 열리게 된다.

post image

이런 식으로 모달을 구현하면 좀 더 앱스러운 UX를 제공할 수 있다. 뒤로가기시에도 모달이 닫히고, URL도 변경되기 때문에 사용자가 뒤로가기 버튼을 눌렀을 때 모달이 닫히는 동작을 자연스럽게 처리할 수 있다.