오픈API의 데이터를 두 개의 페이지에서 사용해야 하는 상황이었는데
axios 요청을 페이지 이동할 때 마다 보내는게 비효율적이라고 생각이 들어서 어떻게 이 문제를 해결할까 고민하다 loader라는 것을 알게되었다.
react-router-dom 6.4v 부터 사용 가능한 기능으로 loader는 컴포넌트가 렌더링 되기 전에 호출되어서 데이터를 return하면 동등한 위치의 컴포넌트와 children 컴포넌트에서 useLoaderData()
로 데이터를 받아서 사용할 수 있다.
router.tsx 파일 생성
loader를 사용하기 위해 createBrowserRouter로 라우터 정보를 담은 객체 배열을 만들어줘야 한다.
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Main from "../pages/Main"; import Mypage from "../pages/Mypage"; import Signin from "../pages/account/Signin"; import InstructorDetailPage from "../pages/InstructorDetailPage"; import Signup from "../pages/account/Signup"; import Webcam from "../pages/Webcam"; import FindId from "../pages/account/FindId"; import PersonalLearning from "../pages/PersonalLearning/PersonalLearning"; import Quiz from "../pages/quiz/Quiz"; import axios from "axios"; import { SignRes } from "../types/interface"; import App from "../App"; const router = createBrowserRouter([ { path: "/", element: <App />, children: [ { index: true, element: <Main /> }, { path: "learning", element: <PersonalLearning />, loader: async () => { const response = await axios.get(url, { params: { serviceKey: apiKey, numOfRows: "10", pageNo: "1", }, }); const items = response.data.response.body.items.item; let results: SignRes[] = items.map((item: SignRes, index: number) => ({ key: index, title: item.title, url: item.url, description: item.description, referenceIdentifier: item.referenceIdentifier, subDescription: item.subDescription, })); return results; }, children: [ { path: "quiz", element: <Quiz />, }, ], }, { path: "login", element: <Signin /> }, { path: "find/id", element: <FindId /> }, { path: "signup/student", element: <Signup role={"student"} /> }, { path: "signup/tutor", element: <Signup role={"tutor"} /> }, { path: "mypage", element: <Mypage /> }, { path: "tutors/:tutorIndex", element: <InstructorDetailPage /> }, { path: "class", element: <Webcam /> }, ], }, ]); export default router;
<RouterProvider router={}/>
를 보내 사용한다.나는 Header와 Footer를 넣어주기 위해 index.ts에 RouterProvider를 넣어줬다.
index.ts
import ReactDOM from "react-dom/client"; import { RouterProvider } from "react-router-dom"; import "./axiosConfig"; import router from "./routes/Router"; const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render(<RouterProvider router={router} />);
App.tsx
import { Outlet } from "react-router-dom"; import Header from "./components/Header"; import Footer from "./components/Footer"; import "./styles/index.scss"; function App() { return ( <> <Header /> <Outlet /> // router.tsx에서 App.tsx의 모든 자식 컴포넌트 <Footer /> </> ); } export default App;
PersonalLearning.tsx
import { useEffect, useRef, useState } from "react"; import Button from "../../components/button/Button"; import ResultCard from "./ResultCard"; import { SignRes } from "../../types/interface"; import { Outlet, useLoaderData, useLocation } from "react-router-dom"; type KORIndexType = { [key: string]: string[]; }; export default function PersonalLearning() { const results = useLoaderData() as SignRes[]; // return 데이터 내려받기 const location = useLocation(); // 자식 컴포넌트에 영향을 주지 않도록 useLocation() 사용 const [searchTerm, setSearchTerm] = useState<string>(""); const [searchResults, setSearchResults] = useState<SignRes[]>([]); const [isSearched, setIsSearched] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<string>(""); const fetchedDataRef = useRef<SignRes[]>([]); const getSignData = async () => { if (fetchedDataRef.current.length > 0) return; setIsLoading(true); try { fetchedDataRef.current = results; // 내려받은 데이터 사용 setSearchResults(results); setError(""); setIsLoading(false); } catch (error) { console.error(error); setSearchResults([]); setError("검색 중 오류가 발생했습니다."); setIsLoading(false); } }; useEffect(() => { getSignData(); }, []); ... (생략) return ( <section> {location.pathname !== "/learning/quiz" && ( // 자식 컴포넌트 경로로 이동했을 때 부모 컴포넌트는 보이지 않게 처리 <> <h2>무엇을 검색하시겠어요?</h2> <div className="search_bar"> <input type="text" placeholder="수어 검색" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> <Button onClick={handleSearch} text="검색" /> {error && <p>{error}</p>} </div> <div className="search_category"> <Button key={"all"} onClick={handleReset} text="전체" /> {Object.keys(KOR).map((keyword) => ( <Button key={keyword} onClick={() => keywordSearch(keyword)} text={keyword} /> ))} </div> <ul> <h3> {isSearched ? "검색 결과" : "전체"} ({searchResults.length}) </h3> {isLoading && <p>데이터를 불러오고 있어요 😀</p>} {searchResults.map((result) => ( <ResultCard {...result} /> ))} </ul> </> )} <Outlet /> // 자식 컴포넌트 (<Quiz />) </section> ); }
참고 자료
[Router] loader와 useLoaderData
리액트 라우터(React Router)란? ( install 설치 / createBrowserRouter 사용법)
React Router v6.8.2 사용법