Router

lee Samse·2024년 7월 2일

react-course

목록 보기
5/8
post-thumbnail
  • React Router는 5.x와 6.x에서 많은 변경이 있다. 여기서는 6.x만 설명한다.
  • Next.js는 CRA와 별개로 제공되는 React framework로 리액트로 앱을 만들기 위한 모든 기능을 제공하며 서버사이드렌더링도 지원한다. 그리고 라우팅을 파일시스템 경로 기반으로 제공한다.

프로젝트에 react-router-dom 추가

npm install react-router-dom

src/index.js에서 BrowserRouter콤포넌트로 App을 감싸준다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

각 화면에 해당하는 페이지 콤포넌트들을 만들어준다.

const Home = () => {
  return (
	  <div>
		  <h1><Home/h1>
		  <p>First page</p>
	  </div>
  );
}

const About = () => {
  return (
	  <div>
		  <h1><About/h1>
		  <p>about page</p>
	  </div>
  );
}

Home과 About화면이 준비 되었다.
이제 특정경로에 위 화면을 연결해 주기 위해 Route 콤포넌트를 사용한다.

<Route path="주소규칙" element={연결할 페이지 콤포넌트} />

App콤포넌트에 Routes로 감싸서 페이지들을 초기화 합니다.

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';

const App = () => {
	return (
		<Routes>
			<Route path="/" element={<Home />} />
			<Route path="/about" element={<About />} />
		</Routes>
	);
}
export default App;

이제 화면간 링크 이동을 하기 위해 Link 콤포넌트를 사용할 수 있습니다.

<Link to="경로">링크 이름</Link>

링크이동을 위해 Home을 수정한다.

const Home = () => {
  return (
	  <div>
		  <h1><Home/h1>
		  <p>First page</p>
		  <Link to="/about">About</Link>
	  </div>
  );
}

페이지 이동간에 데이터를 전달하는 방법은 URL parameter와 Query string이 있다.

createBrowserRouter

Routes에 Route를 배치하는 방법 말고 createBrowserRouter를 사용하여 구성할 수 있다.

createBrowserRouter함수를 사용하여 router객체를 만들어 낸 후 RouterProvider 콤포넌트로 router 객체를 지정하게 함으로서 구성이 가능하다.

const router = createBrowserRouter([
    {
        path: "/",
        element: <App />,
        children: [
            {
                path: "Home",
                element: <Coins />
            },
            {
                path: "Coin",
                element: <Coin />
            }
        ]
    }
]);
export default router;

루트에 해당하는 App 콤포넌트의 Outlet에 Home과 Coin path에 해당하는 Coins와 Coin 페이지가 렌더링될것이다.

index.tsx에 다음과 같이 RouterProvider에 작성된 router를 넘겨준다.

// index.tsx
  <React.StrictMode>
    <ThemeProvider theme={darkTheme}>
      <RouterProvider router={router} />
    </ThemeProvider>
  </React.StrictMode>

루트에 해당하는 App component는 다음과 같다.

// App.tsx
function App() {
  return <div><Outlet /></div>;
}

요약 하면

  • index.tsx에서는 createBrowserRouter함수로 반환된 router객체를 RouterProvider에 인자로 전달하여 렌더링한다.
  • createBrowserRouter는 root를 기반으로 라우팅 구조를 설계한다.
  • App.tsx에서는 Outlet component를 배치하여 서브페이지영역을 설정해준다.
useParams

URL parameter를 전달하여 사용하는 방법은 useParams hook을 사용하는 방법이다.
Profile이라는 화면을 추가하고 해당 화면으로 진입시 url에 profile id를 넘겨주어 프로필화면을 해당 id에 해당하는 사용자의 정보를 표시하는 예를 들겠다.

<Route path="/profiles/:profile_id" element={<Profile/>} />

위와 같이 route를 구성하면 Profile 콤포넌트에서는 다음과 같이 profile_id에 접근할 수 있다.

const Profile = () => {
	const { profile_id } = useParams();
	return {
		<div>
			<p>{profile_id}의 프로필 정보</p>
		</div>
	};
}

Link 를 통해 해당 화면에 접근하려면 다음과 같이 하면 된다.

<Link to="/profiles/1>1번째 프로필</Link>

useLocation

query string으로 페이지 이동간에 데이터를 전달할때 useLocation hook을 사용할 수 있다.

import { useLocation } from 'react-router-dom';

const About = () => {
  const location = useLocation();

  return (
    <div>
      <h1>소개</h1>
      <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
      <p>쿼리스트링: {location.search}</p>
    </div>
  );
};

export default About;

location객체는 다음 값들을 가지고 있다.

  • pathname : 현재주소 경로
  • serach : query string값(?문자 포함)
  • hash : 주소의 # 문자열 뒤의 값
  • state : 페이지 이동간에 임의로 넣을 수 있는 상태값
  • key : location 객체 고유의 값으로 초기에는 default 이후에는 변경될때마다 고유값으로 생성됨.

주소창에서 http://localhost:3000/about?detail=true&mode=1 이라고 치면 location.search에 ?detail=true&mode=1 이란 값이 들어오게 된다.

useSearchParams hook을 사용하면 query string 처리를 더 쉽게 할 수 있다.

import { useSearchParams } from 'react-router-dom';

const About = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const detail = searchParams.get('detail');
  const mode = searchParams.get('mode');

  const onToggleDetail = () => {
	  setSearchParams({
		  mode, 
		  detail: detail === 'true' ? false : true
	  });
  }
  const onIncreaseMode = () => {
    const nextMode = mode === null ? 1 : parseInt(mode) + 1;
    setSearchParams({ mode: nextMode, detail });
  };

  return (
    <div>
      <h1>소개</h1>
      <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
      <p>detail: {detail}</p>
      <p>mode: {mode}</p>
      <button onClick={onToggleDetail}>Toggle detail</button>
      <button onClick={onIncreaseMode}>mode + 1</button>
    </div>
  );
}

useSearchParams는 useParams처럼 배열을 리턴하며 첫번째 객체는 get/set으로 값에 접근하거나 변경할수 있다. 두번째는 함수이며 객체형태로 값을 변경할 수 있고 변경되면 화면이 렌더링 된다.

중첩 된 라우트

게시글목록(/articles), 게시글상세(/articles/article_id)와 같은 주소체계를 사용하고자 한다면 라우트는 다음과 같은 형태를 갖게 될것이다.

    <Route path="/articles" element={<Articles />} />
    <Route path="/articles/:id" element={<Article />} />

그런데 게시글상세에 게시글 목록도 표시하고 싶다면 게시글목록 화면에 게시글 상세화면을 삽입하는 방식을 생각해 볼수 있다. 이는 Outlet 콤포넌트를 이용해서 구현이 가능하다.

라우트는 Route안에 Route를 배치한다.

      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>

그리고 내부 라우트의 콤포넌트가 배치될 위치를 외부라우트 콤포넌트에 Outlet 콤포넌트로 정의해준다.

Articles 콤포넌트에 Outlet을 추가해준다.

import { Link, Outlet } from 'react-router-dom';

const Articles = () => {
  return (
    <div>
      <Outlet />  // <-- 이 위치에 하위 콤포넌트가 배치된다.
      <ul>
        <li>
          <Link to="/articles/1">게시글 1</Link>
        </li>
        <li>
          <Link to="/articles/2">게시글 2</Link>
        </li>
        <li>
          <Link to="/articles/3">게시글 3</Link>
        </li>
      </ul>
    </div>
  );
};

export default Articles;

이와 같이 하면 /articles/1 과 같은 경로에 들어가면 Articles와 Article 콤포넌트가 순서대로 렌더링된다.

useNavigate

Link콤포넌트가 아니라 코드로 화면을 이동하고 싶을 때 사용할 수 있는 hook으로 useNavigate가 있다.

  const navigate = useNavigate();

  const goBack = () => {
    // 이전 페이지로 이동
    navigate(-1); // -2하면 두번뒤로 간다. 1 하면 앞으로 한번 간다.
  };

  const goArticles = () => {
    // articles 경로로 이동
    navigate('/articles');
    // articles 경로로 이동하며 히스토리에 남기지 않음.
    navigate('/articles', { replace: true });
  };

to props의 주소와 동일한 경우 특정 스타일을 주입하고 싶을때 사용한다.
다음은 ArticleItem 콤포넌트가 렌더링된 라우트 경로와 일치 하는경우 activeStyle을 li 태그에 적용하는 예제이다.

import { NavLink, Outlet } from 'react-router-dom';

const Articles = () => {
  return (
    <div>
      <Outlet />
      <ul>
        <ArticleItem id={1} />
        <ArticleItem id={2} />
        <ArticleItem id={3} />
      </ul>
    </div>
  );
};

const ArticleItem = ({ id }) => {
  const activeStyle = {
    color: 'green',
    fontSize: 21,
  };
  return (
    <li>
      <NavLink
        to={`/articles/${id}`}
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      >
        게시글 {id}
      </NavLink>
    </li>
  );
};

export default Articles;
Not Found

라우트에 등록되지 않은 경로로의 접근이 시도될때 표시할 페이지를 정의할 수 있다.

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
};

여기서 * 는 wildcard 문자이며 아무 텍스트나 매칭되는 경우를 커버하며 위 예에서 상단의 라우트들을 모두 통과하는 경로의 경우 이 케이스에 걸리면서 NotFound 콤포넌트를 표시하게 된다.

createBrowserRouter를 사용한다면 다음과 같은 형식이 될것이다.

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "Home",
        element: <Coins />,
        errorElement: <ErrorComponent />
      },
      {
        path: "Coin",
        element: <Coin />
      }
    ],
    errorElement: <NotFound />
  }
]);
export default router;

화면에 진입하는 순간 로그인여부를 확인하여 로그인이 되어 있지 않다면 로그인 페이지로 이동시키고 싶다면 Navigate콤포넌트를 리턴해주면 된다.

다음 MyPage 콤포넌트는 isLoggoedIn이 false라면 Navigate콤포넌트를 리턴하고 있다. 이 때 /login 경로를 지정하고 replace props를 지정하여 히스토리를 삭제하면서 이동하고 있다.

import { Navigate } from 'react-router-dom';

const MyPage = () => {
  const isLoggedIn = false;

  if (!isLoggedIn) {
    return <Navigate to="/login" replace={true} />;
  }

  return <div>마이 페이지</div>;
};

export default MyPage;
useOutletContext

Outlet은 router에서 자식뷰를 렌더링하는 위치를 지정한다. Root에는 그래서 반드시 하나의 Outlet이 필요하다.
root처럼 children을 갖는 자식이 있을 수 있으며 이런 경우 당연히 Outlet이 필요하다.

/coin/1/code
/coin/2/code

와 같은 url표현이 가능하다.

위 샘플에서 Coin 페이지에서 coin의 code를 표시해주는 child를 갖는 라우터는 아래와 같이 정의한다.

{
	path: "coin/:coinId",
	element: <Coin />,
	children: [
		{
			path: "code",
			element: <CoinCode />
		}
	]
},

children이 추가되었고 추가되는 path는 code이며 해당 페이지는 CoinCode이다. 이는 순서대로 Root가 렌더링되고 Root의 Outlet에 Coin이 렌더링되고 인자는 coinId로 전달된다. 그리고 Coin의 Outlet에 CoinCode가 렌더링된다.

function Coin() {
    const [isShowCode, setShowCode] = useState(true);
    const { coinId } = useParams();

    function onToggle() {
        setShowCode(!isShowCode);
    }

    return <div><h1>Coin name is {coinsDB[Number(coinId) - 1].name}</h1>
        <Link to="./code" onClick={onToggle}>See code</Link>
        {isShowCode ? <Outlet context={
            {
                coinId: coinId
            }
        }/> : null}
    </div>
}

Coin page가 위와 같이 수정되었는데 Link가 하나 추가되었고 버튼 이벤트로 onToggle이 호출되면서 Outlet을 보였다 숨겼다 하고 있다.
Link가 상대 경로로 "./code"로 보내고 있기 때문에 바로 아래의 Outlet에 CoinCode가 표시된다.

useOutletContext

Outlet에 context 속성으로 값을 넘기고 있는데 이렇게 하면 Outlet에 표시되는 페이지들에 정보를 넘길 수 있다.
위와 같이 coinId를 넘기면 CoinCode페이지에서는 useOutletContext로 접근이 가능하다.

interface CoinCodeProps {
    coinId: string
}

const CoinCode = () => {
    const {coinId} = useOutletContext<CoinCodeProps>();
    return <h1>{coinsDB[Number(coinId)-1].name} code is {coinsDB[Number(coinId)-1].code}</h1>
}
export default CoinCode;

위 샘플에서는 Props와 함께 사용된 예제이다.
useOutletContext는 Root에도 적용이 가능하기 때문에 darkMode여부나 서비스전체적으로 공유되어도 되는 정보를 저장하는 용도로 사용이 가능하다.

이 때 Root에서 전달한 context는 Coin에서 전달한 context와 별개로 동작한다. 즉 context끼리는 공유가 안된다. 다시말해 Coin과 CoinCode에서 전달받은 context는 서로 다른 값이된다.
만일 CoinCode도 Coin에서 받은 context정보를 필요로 한다면 CoinCode를 위한 Outlet의 context에 추가해주어야 한다.

let context: any = useOutletContext(); // Root에서 받은 context
context["coinId"] = coinId;  // coinId추가
return <div><h1>Coin name is {coinsDB[Number(coinId) - 1].name}</h1>
	<Link to="./code" onClick={onToggle}>See code</Link>
	{isShowCode ? <Outlet context={
		context // 추가된 context를 인자로 넘김
	}/> : null}
</div>

페이지 접근제어

콤포넌트 로딩 시 Navigate를 통해서 페이지 접근제거가 가능하지만 접근제어를 제공하는 별도의 콤포넌트를 만들어서 Route 구성시 접근제어를 정의할 수 있다.

interface IProtectedRoute {
    children: any,
    isAuthenticated: boolean
}
const ProtectedRoute = ({ children, isAuthenticated }: IProtectedRoute) => {
    if (!isAuthenticated) {
      // 로그인이 되어 있지 않다면 리다이렉트
      return <Navigate to="/" />;
    }
    return children;
};

또는

const ProtectedRoute: React.FC<IProtectedRoute> = ({ children, isAuthenticated }) => {
    if (!isAuthenticated) {
      // 로그인이 되어 있지 않다면 리다이렉트
      return <Navigate to="/" />;
    }
    return children;
}

이 콤포넌트를 routes에서 다음과 같이 구성하면 된다.

{
	path: "search/:keyword",
	element: <ProtectedRoute isAuthenticated={isLoggedIn}>
			<Search />
		</ProtectedRoute>
},

isLoggedIn 변수값이 true일때만 Search 콤포넌트로 이동하게 된다.

profile
삼스입니다.

0개의 댓글