[React/Typescript] react-router-dom@6의 loader 사용하기

옹잉·2024년 5월 3일
0

오픈API의 데이터를 두 개의 페이지에서 사용해야 하는 상황이었는데
axios 요청을 페이지 이동할 때 마다 보내는게 비효율적이라고 생각이 들어서 어떻게 이 문제를 해결할까 고민하다 loader라는 것을 알게되었다.

react-router-dom 6.4v 부터 사용 가능한 기능으로 loader는 컴포넌트가 렌더링 되기 전에 호출되어서 데이터를 return하면 동등한 위치의 컴포넌트와 children 컴포넌트에서 useLoaderData()로 데이터를 받아서 사용할 수 있다.

step 1. router 설정

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;

step 2. index.ts 또는 App.tsx 파일에 <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;

step 3. 자식 컴포넌트에서 사용하기

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 사용법

profile
틀리더라도 🌸🌈🌷예쁘게 지적해주세요💕❣️

0개의 댓글