1. 목표 개요
전역 UI 컴포넌트(예: 온보딩 모달, 토스트, 로딩 스피너 등)를 Zustand를 통해 상태로 제어하는 건 잘 알려진 프론트엔드 개발론이다.
실제 렌더링은 단일 위치에서만 구독하도록 구성하는 방식을 공부했으며, Next.js(App Router)와 Vite + React에서의 사용차이점도 비교해보았다.
실제 AITUTOR 서비스의 온보딩(튜토리얼) 모달을 사용자가 어디 페이지에 있든 전역적으로 컴포넌트를 렌더링해야하는 모달을 만들어
렌더링하는 것이 목적이다.
2. 상태 관리: Zustand store 예시
Store 함수를 만들어, 전역 상태관리 스토어를 만든다.
이 상태들을 사용하는 쪽에서는 store 함수를 마치 커스텀 훅처럼 사용하기에, zustand custom Hook이라고도 불린다.
ts 환경이기에, 전역 상태에 대한 인터페이스(타입)을 선언한 후, create 함수와 set 매개변수(함수) 로 상태에 대한 setter 함수를 선언한다.
// store/useOnboardingStore.ts
import { create } from 'zustand';
interface OnboardingStore {
isOpen: boolean;
open: () => void;
close: () => void;
}
export const useOnboardingstore = create<OnboardingStore>((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}));
3. UI 렌더링 컴포넌트 예시
온보딩 모달 컴포넌트는, 모든 페이지 컴포넌트에서 접근이 가능해야하는 전역 컴포넌트이다.
이때, 전역 컴포넌트는, 보통 전역 상태( zustand store)에 구독하도록 설계한다.
위의 OnboardingStore에서 선언한 global state가 일종의 트리거 역할을 하게끔 하는 것이다.
useModalStore.ts의 isOpen 값을 변경되면 OnBoardingModal.tsx는 그 값을 구독하고 있기에
렌더링이 자동으로 바뀌는 것이다.
// components/molecules/OnBoardingModal.tsx
"use client";
import React from 'react';
import { useOnboardingstore } from '@/app/store/useOnboardingStore';
const OnBoardingModal: React.FC = () => {
const { isOpen, close } = useOnboardingstore(); //전역 상태 구독
if (!isOpen) return null;
return (
<div onClick={close} className="fixed inset-0 bg-black/50 z-50">
<div onClick={(e) => e.stopPropagation()} className="bg-white p-4">
온보딩 모달입니다.
<button onClick={close}>닫기</button>
</div>
</div>
);
};
export default OnBoardingModal;
4. 전역 컴포넌트 렌더링 (Next.js vs Vite)
4.1 Next.js (App Router)
Next.js에서는 app/layout.tsx가 모든 페이지 공통 UI를 렌더링하는 전역 entry point 역할을 한다. 여기에서 전역 컴포넌트를 등록하는 것이 핵심이다.
// app/layout.tsx
"use client";
import Sidebar from '@/app/components/utils/Sidebar';
import OnBoardingModal from '@/app/components/molecules/OnBoardingModal';
import { Toaster } from 'react-hot-toast';
import { usePathname } from 'next/navigation';
import { PracticeProvider } from '@/app/context/PracticeContext';
import useAuthInterceptor from '../hooks/auth/useAuthInterceptor';
import '@/app/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isLoginPage = pathname.startsWith('/login');
useAuthInterceptor();
return (
<PracticeProvider>
<Toaster />
<OnBoardingModal /> {/* 전역 모달 렌더링 */}
<div className="flex bg-black">
{!isLoginPage && <Sidebar />}
<div className="flex flex-col w-full">
<div className="flex-1 bg-black-90">{children}</div>
</div>
</div>
</PracticeProvider>
);
}
이 구조에선 Sidebar는 useOnboardingstore().open()을 호출만 하고, 실제 모달을 렌더링하는 건 layout.tsx의 OnBoardingModal이 담당한다.
4.2 Vite + React 환경
Vite에서는 main.tsx가 앱의 진입점이며, 그 아래 App.tsx에서 전역 컴포넌트를 등록하는 방식이 일반적이다.
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// App.tsx
import Sidebar from './components/Sidebar';
import OnBoardingModal from './components/OnBoardingModal';
import { useLocation } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
function App() {
const location = useLocation();
const isLoginPage = location.pathname.startsWith('/login');
return (
<div>
<Toaster />
<OnBoardingModal /> {/* 상태에 따라 표시됨 */}
<div className="flex bg-black">
{!isLoginPage && <Sidebar />}
<div className="flex-1 bg-black-90">
{/* 라우터 outlet 자리 */}
</div>
</div>
</div>
);
}
export default App;
5. 비유로 이해하기 (전등 스위치, 전동, 전선 연결 인프라)
- useOnboardingstore().open()은 "전등 스위치"다. 여러 컴포넌트에서 누를 수 있다.
- OnBoardingModal은 "전등 자체"다. 단 하나만 존재하고, 상태에 따라 켜짐/꺼짐이 결정된다.
- layout.tsx나 App.tsx는 "전선이 연결된 방"이다. 컴포넌트가 실제로 존재하고 렌더링되는 공간이다.
따라서 스위치는 여러 곳에 있어도, 전등은 하나만 필요하다.
6. 정리
항목 Next.js Vite(React)
전역 UI 컴포넌트 배치 위치 | app/layout.tsx | App.tsx |
Zustand store 정의 | 동일 (create) | 동일 (create) |
전역 구독 컴포넌트 | OnBoardingModal | OnBoardingModal |
상태 변경 호출 위치 | Sidebar, 버튼 등 어디든 | 동일 |
상태 변경 방식 | useOnboardingstore().open() | 동일 |
전역 상태 기반 UI는 단일 렌더링 책임 컴포넌트를 루트에 두고, 상태는 어디서든 조작 가능한 구조로 만들면 유지보수성과 확장성이 높아진다.
'FrontEnd Develop > Project : AI TUTOR' 카테고리의 다른 글
STT 라우팅과 비동기 처리 분리하기: 실전 리팩토링 사례 (0) | 2025.05.05 |
---|---|
튜토리얼 모달 컴포넌트 기능 개발 #2. 상세 설계서 (0) | 2025.03.11 |
튜토리얼 모달 컴포넌트 기능 개발 #1. 기본 설계서 (0) | 2025.03.09 |
Next.js에서 window와 localStorage 사용 시 서버 동작 고려하기 (1) | 2025.03.07 |
Next.js + Typescript 적응기 #1: 프로젝트 구조와 개념 정리 (0) | 2025.03.06 |