React에서 페이지 이동 방법
페이지 전환이 필요한 경우 react-router-dom을 사용하는 것이 가장 좋습니다.
1. React Router (react-router-dom)
React에서 페이지 이동을 구현하는 표준 라이브러리.
설치
npm install react-router-dom
기본 사용법
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
const Home = () => <h1>홈 페이지</h1>;
const About = () => <h1>소개 페이지</h1>;
const App = () => {
return (
<BrowserRouter>
<nav>
<Link to="/">홈</Link> | <Link to="/about">소개</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
};
export default App;
설명
- <BrowserRouter> → 라우팅 기능 활성화
- <Routes> → 여러 개의 <Route>를 감싸는 컨테이너
- <Route path="/" element={<Home />} /> → URL이 /이면 <Home />을 렌더링
- <Link to="/about">소개</Link> → <a> 대신 사용 (페이지 리로드 없이 이동 가능)
2. useNavigate()를 이용한 프로그래밍 방식 이동
Link 대신 버튼 클릭 시 페이지 이동을 원한다면 useNavigate() 훅을 사용할 수 있음.
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
return (
<div>
<h1>홈 페이지</h1>
<button onClick={() => navigate("/about")}>소개 페이지로 이동</button>
</div>
);
};
export default Home;
설명
- useNavigate()를 사용하면 페이지 리로드 없이 동적으로 이동 가능
- navigate(-1) → 뒤로 가기 (브라우저 history.back()과 동일)
- navigate("/about", { replace: true }) → 현재 URL을 대체하여 이동 (뒤로 가기 X)
3. URL 파라미터와 쿼리스트링
1) URL 파라미터 (:id)
동적인 페이지 이동을 구현할 때 사용 (/profile/:username)
import { useParams } from "react-router-dom";
const Profile = () => {
const { username } = useParams();
return <h1>{username}님의 프로필</h1>;
};
// 라우팅 설정
<Routes>
<Route path="/profile/:username" element={<Profile />} />
</Routes>;
/profile/mars → "mars님의 프로필"
/profile/jimin → "jimin님의 프로필"
2) 쿼리스트링 (?key=value)
useLocation()을 사용하여 URL 쿼리스트링 값을 가져올 수 있음.
import { useLocation } from "react-router-dom";
const SearchPage = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const keyword = queryParams.get("q");
return <h1>검색 결과: {keyword}</h1>;
};
// 예: /search?q=React
쿼리스트링 값 읽기
- /search?q=React → "검색 결과: React"
- /search?q=JavaScript → "검색 결과: JavaScript"
4. useEffect()와 함께 useNavigate() 활용
페이지 로드 후 자동으로 이동하고 싶다면 useEffect()와 useNavigate()를 함께 사용.
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const RedirectComponent = () => {
const navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate("/home");
}, 3000); // 3초 후 자동 이동
}, [navigate]);
return <h1>3초 후 홈으로 이동합니다...</h1>;
};
3초 후 /home 페이지로 자동 이동됨.
React Router vs Window.location
| React Router (react-router-dom) | 페이지 리로드 없이 부드러운 전환, SPA 최적화 | 라이브러리 설치 필요 |
| window.location.href | 간단한 방식, 모든 환경에서 작동 | 페이지가 새로고침됨 (SPA 아님) |
window.location.href (비추천)
window.location.href = "/about"; // 새로고침이 발생함!
React에서는 react-router-dom을 사용하는 것이 훨씬 좋음
React Router (react-router-dom) 설정 및 사용법
1. 프로젝트 폴더 구조
React에서 페이지 이동을 관리하려면 폴더 구조를 깔끔하게 정리하는 것이 중요합니다.
📂 src
┣ 📂 components → (공통 UI 컴포넌트)
┣ 📂 pages → (각 페이지 컴포넌트)
┃ ┣ 📄 Home.js
┃ ┣ 📄 About.js
┃ ┣ 📄 Profile.js
┃ ┗ 📄 NotFound.js
┣ 📂 routes → (라우터 관련 설정)
┃ ┗ 📄 AppRouter.js
┣ 📄 App.js → (최상위 컴포넌트)
┗ 📄 index.js → (ReactDOM 렌더링)
pages/ → 페이지별 컴포넌트 (Home.js, About.js, Profile.js)
routes/ → AppRouter.js에서 라우팅 설정을 관리
App.js → AppRouter.js를 불러와서 사용
2. react-router-dom 설치
npm install react-router-dom
3. 페이지 컴포넌트 만들기 (pages/)
라우팅할 페이지를 만들고, 각각을 export default 해야 합니다.
Home.js (홈 페이지)
const Home = () => {
return <h1>홈 페이지</h1>;
};
export default Home;
About.js (소개 페이지)
const About = () => {
return <h1>소개 페이지</h1>;
};
export default About;
Profile.js (동적 라우팅)
URL에 따라 다른 내용을 표시하는 동적 페이지 (예: /profile/사용자이름)
import { useParams } from "react-router-dom";
const Profile = () => {
const { username } = useParams(); // URL의 :username 값 가져오기
return <h1>{username}님의 프로필 페이지</h1>;
};
export default Profile;
/profile/mars → "mars님의 프로필 페이지"
/profile/jimin → "jimin님의 프로필 페이지"
NotFound.js (404 페이지)
없는 페이지에 접근했을 때 표시할 Not Found 페이지 (기본적으로 * 경로로 설정)
const NotFound = () => {
return <h1>404: 페이지를 찾을 수 없습니다.</h1>;
};
export default NotFound;
4. AppRouter.js에서 라우팅 설정
React Router를 설정하는 핵심 부분!
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "../pages/Home";
import About from "../pages/About";
import Profile from "../pages/Profile";
import NotFound from "../pages/NotFound";
const AppRouter = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/profile/:username" element={<Profile />} />
<Route path="*" element={<NotFound />} /> {/* 404 페이지 */}
</Routes>
</BrowserRouter>
);
};
export default AppRouter;
라우팅 방식
- / → <Home />
- /about → <About />
- /profile/:username → <Profile /> (예: /profile/희성)
- * → <NotFound /> (없는 페이지)
5. App.js에서 AppRouter 불러오기
이제 AppRouter를 최상위 App.js에서 불러와서 사용합니다.
import AppRouter from "./routes/AppRouter";
const App = () => {
return <AppRouter />;
};
export default App;
App.js는 AppRouter를 불러오기만 하면 됩니다.
6. Link와 useNavigate로 페이지 이동
1) Link를 사용한 네비게이션
React에서는 <a> 태그 대신 <Link>를 사용하여 페이지 이동을 구현합니다.
import { Link } from "react-router-dom";
const Navbar = () => {
return (
<nav>
<Link to="/">홈</Link> |
<Link to="/about">소개</Link> |
<Link to="/profile/희성">프로필</Link>
</nav>
);
};
export default Navbar;
to="/" → 홈 이동
to="/about" → 소개 페이지 이동
to="/profile/희성" → 동적 프로필 페이지 이동
2) useNavigate()를 사용한 프로그래밍 방식 이동
버튼 클릭 시 동적으로 페이지 이동을 하고 싶다면 useNavigate()를 사용합니다.
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
return (
<div>
<h1>홈 페이지</h1>
<button onClick={() => navigate("/about")}>소개 페이지로 이동</button>
</div>
);
};
export default Home;
navigate("/about") → 소개 페이지로 이동
navigate(-1) → 이전 페이지로 이동 (history.back()과 동일)
7. URL 쿼리스트링 사용 (?search=React)
React Router에서는 useLocation()을 사용하여 쿼리스트링 값을 읽을 수 있습니다.
import { useLocation } from "react-router-dom";
const SearchPage = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const keyword = queryParams.get("q");
return <h1>검색 결과: {keyword}</h1>;
};
export default SearchPage;
/search?q=React → "검색 결과: React"
/search?q=JavaScript → "검색 결과: JavaScript"
React 프로젝트에서 react-router-dom을 사용할 때, 일반적으로 main.tsx (또는 index.js)에서 BrowserRouter로 전체 앱을 감싸는 것이 베스트 프랙티스입니다.
이는 라우팅 상태를 최상위에서 관리하고, 다른 Provider(예: Redux, Context API 등)와 함께 감싸서 일관된 상태를 유지할 수 있기 때문입니다.
1. main.tsx에서 BrowserRouter로 감싸기
React 프로젝트에서 main.tsx는 애플리케이션의 **진입점(entry point)**입니다.
main.tsx (또는 index.tsx)
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
최상위에서 BrowserRouter로 감싸면, 모든 하위 컴포넌트에서 react-router-dom을 사용할 수 있음.
라우터 설정을 App.tsx 또는 AppRouter.tsx로 분리하여 관리하는 것이 좋음.
2. App.tsx에서 라우팅 설정
일반적으로 App.tsx에서 Routes와 Route를 설정합니다.
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/profile/:username" element={<Profile />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
};
export default App;
main.tsx에서 BrowserRouter로 감싸고, App.tsx에서 라우트 설정을 하면 깔끔한 구조가 됩니다.
3. 다른 Provider들과 함께 감싸기
react-router-dom을 사용할 때, Redux Provider나 Context API Provider를 함께 사용할 수도 있습니다.
Redux와 함께 사용하기 (Provider)
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./store"; // Redux Store
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
Redux의 Provider를 BrowserRouter보다 바깥에 두어 전역 상태 관리가 가능하게 함.
라우팅과 Redux를 함께 사용할 때 가장 일반적인 구조.
Context API와 함께 사용하기
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { UserProvider } from "./context/UserContext"; // Context API Provider
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<UserProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</UserProvider>
</React.StrictMode>
);
Context API를 사용하면 전역적으로 사용자 데이터를 관리할 수 있음.
Provider를 BrowserRouter보다 바깥에 두면 전역 상태를 유지할 수 있음.
4. HashRouter vs BrowserRouter
React Router에는 BrowserRouter 외에도 HashRouter가 있습니다.
| BrowserRouter | HTML5 history.pushState API를 사용 | /about, /profile/username |
| HashRouter | URL에 #을 포함하여 사용 (서버 설정 불필요) | #/about, #/profile/username |
일반적인 SPA(싱글 페이지 애플리케이션)에서는 BrowserRouter를 사용하지만,
정적 페이지 또는 서버 설정 없이 배포해야 할 경우(GitHub Pages) HashRouter 사용 가능
import { HashRouter } from "react-router-dom";
<HashRouter>
<App />
</HashRouter>;
특정 부분만 리렌더링하는 방법
- Sidebar는 고정된 컴포넌트로 유지.
- Outlet을 활용하여 특정 부분만 동적으로 변경.
- BrowserRouter는 전체 앱을 감싸되, Sidebar는 루트 레벨에 배치.
- 페이지 이동 시 Sidebar는 그대로 유지되고, Outlet 영역만 리렌더링됨.
1. 폴더 구조
📂 src
┣ 📂 components → (공통 UI 컴포넌트)
┃ ┗ 📄 Sidebar.tsx
┣ 📂 pages → (변경될 페이지 컴포넌트)
┃ ┣ 📄 Dashboard.tsx
┃ ┣ 📄 Settings.tsx
┃ ┗ 📄 Profile.tsx
┣ 📂 routes → (라우터 설정)
┃ ┗ 📄 Layout.tsx → (Sidebar를 포함한 레이아웃)
┣ 📄 App.tsx → (라우터 설정)
┣ 📄 main.tsx → (ReactDOM 설정)
2. Sidebar.tsx (고정될 사이드바 컴포넌트)
import { Link } from "react-router-dom";
const Sidebar = () => {
return (
<nav className="sidebar">
<h2>내비게이션</h2>
<ul>
<li><Link to="/dashboard">대시보드</Link></li>
<li><Link to="/profile">프로필</Link></li>
<li><Link to="/settings">설정</Link></li>
</ul>
</nav>
);
};
export default Sidebar;
<Link>를 사용하여 페이지 이동
Sidebar는 항상 고정된 상태로 유지됨
3. Layout.tsx (Sidebar 포함한 레이아웃)
React Router의 Outlet을 사용하여 Sidebar를 제외한 부분만 변경하도록 설정.
import { Outlet } from "react-router-dom";
import Sidebar from "../components/Sidebar";
const Layout = () => {
return (
<div className="layout">
<Sidebar />
<main className="content">
<Outlet /> {/* 이 부분만 변경됨 */}
</main>
</div>
);
};
export default Layout;
<Sidebar />는 고정되고,
<Outlet />은 변경될 페이지가 렌더링되는 부분
4. App.tsx (라우팅 설정)
Layout.tsx를 부모 컴포넌트로 감싸서 Sidebar는 고정하고 특정 부분만 변경하도록 설정.
import { Routes, Route, BrowserRouter } from "react-router-dom";
import Layout from "./routes/Layout";
import Dashboard from "./pages/Dashboard";
import Profile from "./pages/Profile";
import Settings from "./pages/Settings";
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
};
export default App;
Layout을 <Route path="/" element={<Layout />}>로 감싸 Sidebar가 유지됨
<Outlet /> 부분만 dashboard, profile, settings 페이지로 변경됨
5. main.tsx에서 BrowserRouter 감싸기
main.tsx에서는 BrowserRouter로 App을 감싸기만 하면 됨.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Protected Route와 중첩 라우트(Nested Route)
1. Protected Route (인증된 사용자만 접근 가능)
개념
- 로그인이 된 사용자만 특정 페이지에 접근할 수 있도록 막는 라우팅 방식
- 일반적으로 isLoggedIn, user 같은 상태를 기준으로 판단
📁 예시 구성
src/
├── pages/
│ ├── Login.tsx
│ ├── Dashboard.tsx ← 보호된 페이지
├── store/authStore.ts ← 로그인 상태 저장
├── routes/ProtectedRoute.tsx
└── App.tsx
ProtectedRoute.tsx
// routes/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
interface Props {
children: React.ReactNode;
}
const ProtectedRoute = ({ children }: Props) => {
const { isLoggedIn } = useAuthStore();
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
export default ProtectedRoute;
App.tsx에서 사용
import ProtectedRoute from './routes/ProtectedRoute';
import Dashboard from './pages/Dashboard';
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
2. Nested Route (중첩 라우트)
개념
- 한 페이지 안에서 자식 경로에 따라 다른 화면을 보여줄 수 있음
- 탭 구조, 다단계 페이지 구성 등에 유용
📁 예시 구성
src/
├── pages/
│ ├── Settings/
│ │ ├── SettingsLayout.tsx
│ │ ├── Profile.tsx
│ │ └── Password.tsx
SettingsLayout.tsx
// SettingsLayout.tsx
import { Outlet, Link } from 'react-router-dom';
function SettingsLayout() {
return (
<div>
<h2>설정</h2>
<nav>
<Link to="profile">프로필</Link>
<Link to="password">비밀번호</Link>
</nav>
<Outlet /> {/* 자식 라우트 출력 자리 */}
</div>
);
}
App.tsx 설정
import SettingsLayout from './pages/Settings/SettingsLayout';
import Profile from './pages/Settings/Profile';
import Password from './pages/Settings/Password';
<Route path="/settings" element={<SettingsLayout />}>
<Route path="profile" element={<Profile />} />
<Route path="password" element={<Password />} />
</Route>
- /settings 접속 시 SettingsLayout이 보이고
- /settings/profile, /settings/password로 접속하면 각각의 자식 컴포넌트가 <Outlet />에 렌더링됨
| Protected Route | 로그인 등 인증 상태에 따라 접근을 제한 |
| Nested Route | 부모 컴포넌트 내부에 자식 라우트 구조 렌더링 (Outlet) |
이걸 응용하면:
- /admin 전체를 Protected로 만들고
- 그 안에 /admin/users, /admin/settings 등 중첩 구조로 만들 수도 있어요.
1. 관리자 전용 보호 라우트 (Admin Protected Route)
예: /admin 경로는 isAdmin === true인 유저만 접근 가능
// routes/AdminRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
interface Props {
children: React.ReactNode;
}
const AdminRoute = ({ children }: Props) => {
const { isLoggedIn, user } = useAuthStore();
if (!isLoggedIn || user?.role !== 'admin') {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
export default AdminRoute;
// App.tsx
import AdminRoute from './routes/AdminRoute';
import AdminDashboard from './pages/admin/AdminDashboard';
<Route
path="/admin"
element={
<AdminRoute>
<AdminDashboard />
</AdminRoute>
}
/>
2. useParams()로 URL 동적 파라미터 사용
예: /users/:id 경로에서 유저 ID를 파라미터로 사용
// pages/UserDetail.tsx
import { useParams } from 'react-router-dom';
function UserDetail() {
const { id } = useParams<{ id: string }>();
return <h1>유저 상세 페이지: ID = {id}</h1>;
}
export default UserDetail;
// App.tsx
import UserDetail from './pages/UserDetail';
<Route path="/users/:id" element={<UserDetail />} />
- /users/3 → id = 3
- /users/banana → id = banana
useParams()는 보통 getUserById(id) 등의 API와 함께 사용됩니다.
3. 라우팅 기반 모달 (모달을 URL로 컨트롤)
개념
- URL이 /posts/123이면 PostModal이 뜨도록 설정
- /posts에서는 리스트만 보이고, /posts/:id에서는 모달과 함께 리스트가 보임
예시
// pages/PostsPage.tsx
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import PostModal from './PostModal';
const PostsPage = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const posts = [
{ id: '1', title: '첫 번째 게시글' },
{ id: '2', title: '두 번째 게시글' },
];
return (
<div>
<h1>게시글 목록</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<button onClick={() => navigate(`/posts/${post.id}`)}>
{post.title}
</button>
</li>
))}
</ul>
{/* 라우팅 기반 모달 */}
{id && <PostModal postId={id} onClose={() => navigate('/posts')} />}
<Outlet />
</div>
);
};
export default PostsPage;
// pages/PostModal.tsx
interface Props {
postId: string;
onClose: () => void;
}
const PostModal = ({ postId, onClose }: Props) => {
return (
<div style={{
position: 'fixed', top: '20%', left: '30%',
width: '40%', padding: '2rem', background: 'white',
boxShadow: '0 0 10px rgba(0,0,0,0.3)'
}}>
<h2>게시글 상세: {postId}</h2>
<p>게시글 내용...</p>
<button onClick={onClose}>닫기</button>
</div>
);
};
export default PostModal;
// App.tsx
<Route path="/posts" element={<PostsPage />}>
<Route path=":id" element={<></>} /> {/* 모달 처리를 위한 dummy route */}
</Route>
| 관리자 전용 라우트 | 로그인 + 관리자 권한 체크 후 페이지 접근 허용 |
| useParams() | 동적 URL 파라미터(:id) 추출 |
| 라우팅 기반 모달 | 특정 URL일 때만 모달을 띄워주고, URL 변경으로 닫기 |
useSearchParams 중심으로 정리
1) 기본 사용: 읽기/쓰기
import { useSearchParams } from "react-router-dom"
function Page() {
const [searchParams, setSearchParams] = useSearchParams()
// 읽기
const page = Number(searchParams.get("page") ?? 1)
const q = searchParams.get("q") ?? ""
// 쓰기(전체 교체)
const goFirst = () => setSearchParams({ page: "1" })
// 쓰기(기존 유지 + 일부만 변경)
const setPage = (n: number) => {
const next = new URLSearchParams(searchParams) // 기존 쿼리 보존
next.set("page", String(n))
setSearchParams(next)
}
return (
<>
<button onClick={() => setPage(page + 1)}>다음</button>
<button onClick={goFirst}>처음으로</button>
<div>page={page}, q={q}</div>
</>
)
}
- setSearchParams({ ... }) 객체를 넣으면 기존 쿼리를 전부 교체합니다.
- 기존 파라미터 유지하려면 new URLSearchParams(searchParams)로 복사 후 set()/delete() 하세요.
2) 파라미터 추가/수정/삭제 패턴
// 추가 또는 변경
const setKeyword = (keyword: string) => {
const next = new URLSearchParams(searchParams)
if (keyword) next.set("q", keyword)
else next.delete("q") // 빈값이면 삭제
setSearchParams(next)
}
// 여러 값 관리(?tag=a&tag=b)
const addTag = (tag: string) => {
const next = new URLSearchParams(searchParams)
next.append("tag", tag) // 동일 키 다중 값 허용
setSearchParams(next)
}
// 전체 초기화
const clearAll = () => setSearchParams({})
3) replace 네비게이션(뒤로가기 스택 덜어내기)
// 기본은 push(히스토리 쌓임), replace로 대체 가능
setSearchParams({ page: "2" }, { replace: true })
- 검색 폼에서 입력할 때마다 히스토리가 쌓이지 않게 하려면 replace: true가 유용합니다.
4) UI 상태와 URL 동기화(모달, 토글 등)
4-1) URL 기반 모달 열고 닫기
import { useSearchParams } from "react-router-dom"
function Posts() {
const [searchParams, setSearchParams] = useSearchParams()
const openedId = searchParams.get("postId") // 있으면 모달 오픈
const openModal = (id: string) => {
const next = new URLSearchParams(searchParams)
next.set("postId", id)
setSearchParams(next) // push
}
const closeModal = () => {
const next = new URLSearchParams(searchParams)
next.delete("postId")
setSearchParams(next, { replace: true }) // 닫기는 replace 권장
}
return (
<>
<button onClick={() => openModal("1")}>1번 게시글</button>
{openedId && (
<PostModal
postId={openedId}
onClose={closeModal}
/>
)}
</>
)
}
- 열기: postId 추가
- 닫기: postId 제거(+ replace: true로 뒤로가기 이력 최소화)
4-2) 카드 토글 + URL 동기화(토글-안정버전)
function Card({ id }: { id: number }) {
const [isOpen, setIsOpen] = useState(false)
const [searchParams, setSearchParams] = useSearchParams()
const onClick = () => {
setIsOpen(prev => {
const nextOpen = !prev
const next = new URLSearchParams(searchParams)
if (nextOpen) next.set("id", String(id))
else next.delete("id")
setSearchParams(next, { replace: !nextOpen }) // 닫기는 replace
return nextOpen
})
}
return <button onClick={onClick}>열기/닫기</button>
}
- setState(prev => next) 콜백으로 토글 후 상태를 기준으로 URL을 업데이트하세요.
5) 필터/정렬/페이지네이션 실전 패턴
type SortKey = "new" | "popular"
type Filters = {
page?: number
q?: string
sort?: SortKey
tags?: string[] // multi-value
}
function useListQuery(): [Filters, (patch: Partial<Filters>, opt?: { replace?: boolean }) => void] {
const [sp, setSP] = useSearchParams()
const filters: Filters = {
page: sp.get("page") ? Number(sp.get("page")) : 1,
q: sp.get("q") ?? "",
sort: (sp.get("sort") as SortKey) ?? "new",
tags: sp.getAll("tag"), // ?tag=a&tag=b
}
const update = (patch: Partial<Filters>, opt?: { replace?: boolean }) => {
const next = new URLSearchParams(sp)
if (patch.page !== undefined) next.set("page", String(patch.page))
if (patch.q !== undefined) {
patch.q ? next.set("q", patch.q) : next.delete("q")
}
if (patch.sort !== undefined) next.set("sort", patch.sort)
if (patch.tags !== undefined) {
next.delete("tag")
for (const t of patch.tags) next.append("tag", t)
}
setSP(next, opt)
}
return [filters, update]
}
function ListPage() {
const [{ page, q, sort, tags }, update] = useListQuery()
// 페이지 이동
const go = (n: number) => update({ page: n })
// 검색어 변경(입력 중 replace 권장)
const onChangeQ = (value: string) => update({ q: value, page: 1 }, { replace: true })
// 태그 토글
const toggleTag = (t: string) => {
const s = new Set(tags)
s.has(t) ? s.delete(t) : s.add(t)
update({ tags: [...s], page: 1 })
}
return (
<>
<input value={q} onChange={(e) => onChangeQ(e.target.value)} />
<button onClick={() => go(page + 1)}>다음 페이지</button>
<button onClick={() => toggleTag("bug")}>#bug</button>
<div>sort={sort}, tags={tags.join(",")}</div>
</>
)
}
6) URL ↔ 전역상태(Zustand) 동기화 팁
- 최초 마운트 시 URL → Zustand로 “한 번” 복사
- 이후에는 URL만 진실의 원천(SSOT) 으로 두고, UI 상호작용은 항상 URL을 갱신(→ 필요하면 useEffect로 전역 상태 최신화)
function useSyncWithStore() {
const [sp] = useSearchParams()
const setData = useRequireStore(s => s.setData)
useEffect(() => {
const id = sp.get("id")
if (!id) return
// id를 기준으로 API 호출 후 결과를 전역 저장
fetch(`/api/requires/${id}`).then(r => r.json()).then(setData)
}, [sp, setData])
}
7) 숫자/불리언 파라미터 안전 파서
export const qp = {
num: (v: string | null, def = 0) => {
if (v == null) return def
const n = Number(v)
return Number.isFinite(n) ? n : def
},
bool: (v: string | null, def = false) => {
if (v == null) return def
return v === "1" || v === "true"
}
}
// 사용
const page = qp.num(searchParams.get("page"), 1)
const show = qp.bool(searchParams.get("show"), true)
8) 성능/UX 사소하지만 중요한 팁
- replace 사용처: 입력 중인 검색폼, 모달 닫기, 정렬 변경처럼 뒤로가기에 흔적을 남기고 싶지 않을 때.
- push 사용처: 페이지네이션, 디테일 진입처럼 사용자 히스토리가 필요한 경우.
- URL에 상태가 많아질수록 초기 진입 북마크/공유가 쉬워지고, QA/디버깅도 편해집니다.
- ESLint “unused expressions”를 피하려면 조건부 실행에 삼항연산자 대신 if/else를 권장합니다.
'JavaScript > React' 카테고리의 다른 글
| useRef, useImperativeHandle (1) | 2025.03.21 |
|---|---|
| useEffect, useLayoutEffect, useInsertionEffect (0) | 2025.03.21 |
| React 기본 개념 (0) | 2025.03.21 |
| React 프로그래밍을 위한 javascript 기본 (0) | 2025.03.21 |
| 서버 트래픽 비용을 줄이고 싶을 때, Squoosh (0) | 2025.03.06 |