React Router v6

kim98111·2022년 2월 15일
0

React

목록 보기
24/28
post-thumbnail

Routing이란?

"라우팅(routing)"이란 사용자가 요청한 URL에 따라 그에 맞는 컴포넌트(UI)를 보여주는 것을 라우팅이라고 합니다. 이때 새로운 페이지를 요청하는 것이 아니라 URL에 맞는 컴포넌트를 렌더링해주어야 합니다.

React는 SPA(Single Page Application)로서 하나의 페이지(HTML)을 갖는 애플리케이션입니다. 즉, SPA는 하나의 HTML 파일, 하나의 URL을 가지며, 이는 절대 바뀌지 않습니다. 이처럼 변하지 않는 URL이 이상적이지 않다고 생각이 들 수 있습니다. 예를 들어, 만약 URL을 다른 사람에게 공유를 하거나 북마크를 하더라도 해당 URL로 접속을 한다면 언제나 시작 페이지를 보여주게 될 것입니다. 즉, 화면의 목적에 따라 그에 맞는 URL을 만들어주어야 합니다.

우리는 기존처럼 하나의 페이지(SPA), 즉 하나의 HTML 페이지만을 사용하면서 URL에 따라서 화면에 보이는 것을 바뀌도록 할 수 있습니다. 이는 URL이 변경되더라도 새로운 HTML 파일을 서버로부터 요청하지도 않으면서 URL에 따라 그에 맞는 화면(UI)을 사용자에게 보여줍니다. 이를 가능케 하는 것이 바로 "React Router"입니다.

react router는 react에 내장된 기능이 아니며, 별도로 설치해야 하는 패키지입니다.

react router 설치하기

react router를 사용하기 위해서 패키지를 설치해야 합니다. 터미널에 아래 명령어를 입력하여 패키지를 설치합니다.

npm install react-router-dom

주의해야할 점으로 react-router가 아닌 react-router-dom을 설치해야주어야 합니다.

URL 경로 정의 및 사용하기

먼저 URL의 구조에 대해 먼저 알아보겠습니다.

파일의 위치와 파일명을 나타내는 "Path to the file"에 따라서 렌더링할 컴포넌트를 결정됩니다. 이 부분을 앞으로는 "경로"라고 부르겠습니다.


react router는 URL의 경로에 따라 그에 맞는 컴포넌트를 불러오기 위해서 사용합니다.

예를 들어, URL이 our-domain.com/일 때, 즉 경로가 /일 때는 Component A를 불러오도록 하고 our-domain.com/product일 때, 즉 경로가 /product일 때는 Component B를 불러오도록 해주는 것입니다. 즉, 도메인 다음에 오는 경로값에 따라 보여지는 UI를 결정하고자 합니다.

참고로 URL의 경로가 변경되고 Route가 활성화되어 컴포넌트를 렌더링할 때 렌더링되는 컴포넌트는 매번 재평가되어 렌더링됩니다. 만약 이전과 동일한 경로로 변경하더라도 react router는 렌더링될 컴포넌트를 재평가하고 리렌더링합니다.

이는 조건적으로 컴포넌트를 렌더링하는 것과 유사하다고 볼 수 있습니다. 특정 URL의 경로에 따라 렌더링할 컴포넌트를 결정하는 것입니다.


// App.js
import { Routes, Route } from 'react-router-dom';

import Welcome from './component/Welcome';
import Products from './component/Products';

function App() {
    return (
        // Route 컴포넌트는 반드시 Routes 컴포넌트로 감싸주어야 합니다.
        <Routes>
            // Route 컴포넌트로 URL의 경로값에 따라 렌더링될 컴포넌트를 설정합니다.
            <Route path="/welcome" element={<Welcome />}>
            <Route path="/products" element={<Products />}>
        </Routes>
    );
}

export default App;

먼저 react-router-dom에서 Route 컴포넌트를 가져옵니다. Route 컴포넌트는 특정 경로일 때 보여질 컴포넌트를 설정해주는 컴포넌트입니다.

Route 컴포넌트는 아래처럼 사용합니다.

// path prop에는 특정 URL의 경로값을 작성
// element prop에는 Route가 활성화될 때 렌더링될 컴포넌트를 작성
<Route path="/경로" element={리액트엘리먼트} />

참고로 react-router-dom v6부터는 Route라는 컴포넌트를 작성할 때 반드시 "Routes라는 컴포넌트로 감싸주어야 합니다". 그리고 일반적으로 Routes 컴포넌트의 자식으로 작성한 Route는 "하나만 활성화"됩니다.

path prop

Route 컴포넌트에는 path라는 prop을 설정해야 합니다. path prop에는 "특정 URL의 경로값"을 작성해줍니다.

경로값을 작성할 때 "절대 경로"뿐만 아니라 "상대 경로"도 작성이 가능합니다. 이때 동작이 서로 다릅니다.

  • 절대경로인 경우 반드시 path prop값과 현재 URL의 경로값이 "일치"한 경우에 Route 컴포넌트가 활성화 된다.

  • 상대 경로인 경우 현재 페이지 컴포넌트를 element prop으로 갖고있는 Route의 path prop을 기준 URL로 사용합니다.
    즉, 활성화된 Route의 element prop에 작성된 페이지 컴포넌트 내부에서 상대 경로 사용시 기준 URL 경로는 해당 Route의 path prop값이 됩니다.


예를 들어, Route의 element prop 값이 Fruits 컴포넌일 때, Fruits 컴포넌트 내부에서 상대 경로 사용시 기준이 되는 URL 경로는 Fruits 컴포넌트를 element prop으로 갖고 있는 Route 컴포넌트의 path prop값"/fruits"을 기준으로 상대 경로가 결정됩니다.

// App.js
// 현재 URL "our-domain.com/"
import { Routes, Route } from 'react-router-dom';

import Fruits from '../pages/Fruits';

const App = () => {
    return (
        <Routes>
            <Route path="/fruits/*" element={<Fruits />} />
            ,,,
        </Routes>
    );
};

export default App;



// Fruits.js
// 현재 URL "our-domain.com/fruits"
import { Routes, Route } from 'react-router-dom';

import Apple from './Apple.js';
import Banana from './Banana.js';

const Fruits = () => {
    return (
        <Routes>
            // 상대 경로 "apple" 사용
            // 현재 페이지 컴포넌트인 Fruits 컴포넌트를
            // element prop으로 갖는 Route 컴포넌트의 path prop값을 기준으로
            // 상대 경로 결정
            // 즉, URL 경로가 "/fruits/apple"일 때 활성화
            <Route path="apple" elemen={<Apple />} />
            
            // 절대 경로 "/banana" 사용
            // URL 경로가 "/banana"일 때 활성화
            <Route path="/banana" element={<Banana />} />
        </Routes>
    );
}

export default Fruits;

element prop

그리고 react router는 Route가 활성되었을 때 어떤 페이지를 화면에 렌더링해야할지 알아야하기 때문에 Route 컴포넌트의 element prop에 "페이지 컴포넌트를 작성"해줍니다. element prop에 작성한 컴포넌트는 Route 컴포넌트가 작성된 위치에 렌더링됩니다.

즉, 현재 URL의 경로가 Route 컴포넌트의 path prop에 작성된 값과 일치할 때 Route 컴포넌트가 활성화 되고, react router가 활성화된 Route 컴포넌트의 element prop에 작성된 컴포넌트가 화면에 렌더링이 됩니다.

react router는 URL의 경로를 평가하고, 그 URL의 경로에 따라 컴포넌트를 렌더링하게 됩니다.

BrowserRouter 컴포넌트

우리는 Route를 활성화 하고 다른 react rouuter 기능을 동작하도록 하기위해서 추가작업이 필요합니다.

// index.js
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './App';

// BrowserRoute 컴포넌트는 react-router-dom가 동작되도록 합니다
ReactDOM.render(<BrowserRouter>
    <App />
</BrowserRouter>, document.getElementById('root'));

react-router-dom이 제공하는 BrowserRouter라는 컴포넌트를 가져옵니다. 그리고 라우팅을 사용할 컴포넌트들을 BrowserRouter 컴포넌트로 감싸줍니다.

위 예제에서는 루트 컴포넌트인 App 컴포넌트를 BrowserRouter 컴포넌트로 감싸주었습니다.

BrowserRoute 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로 불러오지 않고도 URL를 변경하고 현재 URL과 관련된 정보를 리액트 컴포넌트에서 사용할 수 있도록 해 줍니다.

링크 작업하기

우리는 클릭을 하면 어디론가 이동하도록, 즉 URL 경로를 변경하여 그에 맞는 컴포넌트를 보여주도록 만들어주어야 합니다. 따라서 우리는 페이지에 링크를 추가하는 방법을 알아보겠습니다.

// App.js
// App 컴포넌트는 index.html 문서에 렌더링되는 컴포넌트로
// path prop이 "/"인 Route 컴포넌트에 element prop에 속한것과 동일
import { Routes, Route } from 'react-router-dom';

import Welcome from './component/Welcome';
import Products from './component/Products';

function App() {
    return (
        <div>
            <nav>
                <ul>
                    <li>
                        // a 요소를 클릭시 URL의 경로가 /welcome으로 변경되고,
                        // 브라우저가 서버에게 그에 맞는 새로운 페이지를 요청한다
                        <a href="/welcome">Welcome</a>
                    </li>
                    <li>
                        <a href="/products">Products</a>
                    </li>
                </ul>
            </nav>
            
            </header>
            
            <Routes>
                <Route path="/welcome" element={<Welcome />}>
                <Route path="/products" element={<Products />}>
            </Routes>
        </div>
    );
}

export default App;

a 엘리먼트의 href prop에 각 URL의 경로를 작성하면 react router가 그에 맞는 컴포넌트를 렌더링해줍니다.
하지만 이는 큰 단점이 존재하게 됩니다. 링크를 클릭할 때마다 새로운 페이지를 불러와서 컴포넌트를 렌더링하게 됩니다. 즉, 해당 링크에 작성된 URL로 새로운 요청을 보내게 됩니다.

이렇게 링크를 클릭할 때 새로운 HTML 파일을 요청한다는 것은 SPA로 동작하는 애플리케이션이 기존 상태를 모두 잃어버리게 된다는 문제점이 발생합니다. 즉, 기존 상태를 갖고 있는 애플리케이션을 버리고 새롭게 요청을 전송하게 되어버립니다. 이는 SPA 구축과 반대되는 개념이며 이상적이지 않습니다.

예를 들어, 상태로 장바구니 목록을 관리하고 있었다면 링크를 클릭하는 순간 모든 기존 상태를 잃어버리게 되는 문제가 발생할 것입니다.
즉, 요청을 전송하고 새로운 HTML 페이지를 요청하는 브라우저의 기본 동작을 막아야합니다.

우리는 a 엘리먼트에 이벤트 핸들러를 등록하고 기본 동작을 막는다면 우리가 원하는대로 새롭게 HTML 페이지를 요청하지 않고 변경된 URL에 대응하는 컴포넌트를 react router가 렌더링하도록 만들어야 합니다.


이러한 작업을 수동으로 하지 않고 우리는 react-router-dom의 "Link 컴포넌트"를 사용하여 이러한 작업을 처리해줍니다.

// to prop에 특정 경로값을 작성
<Link to="경로">링크 이름</Link>

기존 a 엘리먼트를 사용하는 대신에 Link 컴포넌트로 대체하고 href prop을 "to prop"으로 바꿔줍니다. to prop에도 절대경로와 상대경로를작성할 수 있습니다. 상대경로 사용시 URL의 경로값 기준은 현제 페이지 컴포넌트를 element prop으로 갖는 Route 컴포넌트의 path prop 값이 됩니다.

Link 컴포넌트의 to prop에 작성한 경로값이 "/"로 시작하는 "절대 경로값"인 경우 해당 경로로 "대체"될 것이고, "/"로 시작하지 않는 "상대 경로"인 경우 자신이 속한 Route의 path prop값 뒤에 /와 to prop의 값이 "추가(push)된 경로값"으로 변경될 것입니다.

import { Link } from 'react-router-dom';
,,,
// 현재 URL이 "our-doamin.com/about"라고 가정

// 절대 경로 작성시
// 클릭시 "our-domain.com/apple"로 변경
<Link to="/apple">apple</Link>

// 상대 경로 작성시
// 클릭시 "our-domain.com/about/apple"로 변경
<Link to="banana">banana</banana>

Link 컴포넌트는 실제 돔에는 "a 엘리먼트"로 렌더링되며 내부적으로 클릭시 "HTML을 요청하는 기본 동작을 막는" 로직이 추가되어 있습니다. react-router가 수동으로 URL을 업데이트해주고 그에 맞는 컴포넌트를 화면에 렌더링해줍니다. 따라서 페이지가 전환된 것처럼 보이도록 합니다.

// App.js
import { Routes, Route, Link } from 'react-router-dom';

import Welcome from './component/Welcome';
import Products from './component/Products';

function App() {
    return (
        <div>
        
            <nav>
                <ul>
                    <li>
                        // Link 컴포넌트를 통해 링크를 생성
                        // Link 컴포넌트는 a 엘리먼트를 반환하며
                        // 클릭시 URL만 변경하고 요청을 보내지 않음
                        <Link to="/welcome">Welcome</Linkt>
                    </li>
                    <li>
                        <Link to="/products">Products</Link>
                    </li>
                </ul>
            </nav>
            
            <Routes>
                <Route path="/welcome" element={<Welcome />}>
                <Route path="/products" element={<Products />}>
            </Routes>
        </div>
    );
}

export default App;

이를 통해 우리는 공유할 수 있는 URL을 가질 수 있게 되었고, 이를 브라우저에 입력했을 때 그에 맞는 UI를 화면에 렌더링시켜줍니다.


Link 컴포넌트이 to prop에 경로값을 문자열이 아닌 객체로 전달할 수도 있습니다.

to prop에 작성한 객체는 pathname 프로퍼티에 경로값을 문자열로, search 프로퍼티에는 쿼리파라미터와 값을 문자열로,

react-router-dom의 NavLink 컴포넌트는 기본적으로 Link 컴포넌트와 같습니다. a 엘리먼트를 생성하고 클릭 이벤트를 캐치하여 브라우저의 기본 동작을 막습니다. 또한 추가적인 기능이 존재합니다.

NavLink의 style prop 또는 className prop에 "함수"를 작성하여 스타일을 동적으로 적용할 수 있습니다.

두 prop의 값으로는 "객체를 인수로 전달받는 함수"를 값으로 작성합니다. 인수로 전달받는 객체에는 isActive라는 프로퍼티가 존재하며 NavLink를 클릭시 isActive 프로퍼티 값이 true로 설정됩니다. 그리고 함수의 "반환값으로 클래스 이름이 설정"됩니다.

import { NavLink } from 'react-router-dom';
import calsses from './MainHeader.module.css';

// 인수로 전달받는 객체의 isActive 프로퍼티 값을 통해 
// 동적으로 클래스 명이 설정되도록 작성
<NavLink className={({ isActive }) => isActive ? classes.active : ''} to="/welcome">
    Welcome
</NavLink>

// 인수로 전달받는 객체의 isActive 프로퍼티 값을 통해
// 동적으로 인라인 스타일을 설정
<NavLink style={({ isActive )} => isActive ? ({ color: blue }) : ({ color: red })} to="/welcome">
    Welcome
</NavLink>

isActive는 NavLink가 클릭이 되면 true, 아니면 false값을 갖습니다.

동적 라우팅

"동적 라우팅"이란 "하나의 Route"를 사용하되, URL의 경로에 따라 Route의 path prop의 값이 동적으로 결정되면 element prop에 작성된 컴포넌트에서는 동적으로 결정된 경로값을 통해 컴포넌트에 렌더링될 정보를 결정하는 것입니다.

즉, Route가 불러와야 하는 페이지 컴포넌트의 구체적인 정보를 URL의 경로에 따라 동적으로 결정되도록 만들어 주는 것입니다. 동적 라우팅을 사용함으로써 우리는 하나의 Route 컴포넌트를 "재사용"할 수 있다는 장점이 있습니다.

예를 들어, 어떤 상품 리스트가 존재합니다. 각 리스트를 클릭하면 그 상품에 대한 상세 페이지로 넘어가도록 하기 위해서 우리는 각 상품의 개수만큼 상세 페이지 컴포넌트를 만들어 Route를 설정할 수 있지만 상품 개수가 많아진다면 이는 비효율적일 것입니다.

경로 파라미터를 이용한 동적 라우팅 페이지

우리는 동적 라우팅을 사용하여 "하나의 Route를 사용"하고, URL의 경로에 따라 각 상품의 상세 "페이지에 들어갈 내용을 동적으로 결정"합니다. 즉, 현재 URL의 경로값에 따라 Route 컴포넌트의 path prop의 값이 동적으로 결정되면, element prop에 작성된 컴포넌트 내부에서는 동적으로 결정된 경로값을 통해 가져올 정보를 결정합니다.

<Route path="/경로/:경로파라미터" element={<Component />} />

Route 컴포넌트의 path prop에 ":경로파라미터가 동적으로 결정"되는 부분입니다. 즉, : 뒷 부분이 "경로 파라미터"가 됩니다.

예를 들어, 변경한 URL이 "our-domain.com/path/abc"라면 <Route path="/path/:pathParam" element={<Component />} />인 Route가 활성화 되고, 경로 파라미터인 pathParam에는 abc가 할당됩니다. 만약 "/path/q1"으로 변경되었다면 동일한 Route가 활성화 되고, 경로 파라미터 pathParam에는 q1이라는 값이 할당됩니다.


// App.js
// 현재 URL은 "our-domain.com/"
import { Routes, Route, Link } from 'react-router-dom';

import Welcome from './components/Welcome';
import Products from './components/Products';
import ProductDetail from './Components/ProductDetail';

const App = () => {
    return (
        <div>
            <ul>
                <li>
                    // 절대 경로이므로 replace
                    <Link to="/products">Products List</ Link>
                </li>
                <li>
                    // 절대 경로이므로 replace
                    <Link to="/welcome">Welcome</Link>
                </li>
            </ul>
            
            // element prop에 작성한 컴포넌트들은 각 Route 컴포넌트 위치에 렌더링
            <Routes>
                <Route path="/welcome" element={<Welcome />} />
                <Route path="/products" element={<Products />} />
                
                // path prop에 :productId가 경로 파라미터
                // :productId 부분에는 어떤 값이든 올 수 있으며
                // 경로 파라미터에 어떤 값이 오든 해당 Route가 활성되된다
                <Route path="/products/:productId" element={<ProductDetail />} />
            </Routes>
        </ div>
    );
}

export default App;

Products List라는 링크를 누르게 되면 URL이 "our-domain/products"로 바뀌게 되고 "/products"를 path prop으로 갖고 있는 Route 컴포넌트가 활성화 됩니다. 이후 react router가 활성화된 Route 컴포넌트의 element prop에 작성된 컴포넌트를 화면에 렌더링해줍니다.


// Products.js
// 현재 URL은 "our-domain.com/products"
import { Link } from 'react-router-dom';

const Products = () => {
    return (
        <section>
            <h1>The Products Page</h1>
            <ul>
                <li>
                    // 상대 경로인 점에 주의
                    // "/products/p1"과 동일
                    <Link to="p1">A Book</Link>
                </li>
                <li>
                    // "/products/p2"과 동일
                    <Link to="p2">A Carpet</Link>
                </li>
                <li>
                    // "/products/p3"과 동일
                    <Link to="p3">An Online Course</Link>
                </li>
            </ul>
        </section>
    );
}

export default Products;

Products 컴포넌트는 상품 리스트를 렌더링하는 컴포넌트입니다. 각 상품을 Link 컴포넌트 렌더링하였습니다. 만약 A Book 링크를 클릭하면 URL이 our-domain/products/p1으로 변경이 될 것입니다.

URL이 변경되고 App 컴포넌트에 작성된 Route 컴포넌트 중에서 path prop의 값이 /products/:productId인 Route가 활성화가 됩니다. 이때 productId 부분이 p1으로 결정되고 Route가 활성화 됩니다.

여기서 알 수 있는 점으로 path prop에서 : 뒤에 작성한 것이 URL에 따라 동적으로 값이 결정된다는 것입니다. 즉, productId가 변수처럼 URL에 따라 동적으로 값이 할당됩니다.

여기서 주의해야할 점이 있습니다. 만약 우리가 our-domain/products라는 URL을 갖고 있다면 Routes 컴포넌트 내부에 작성된 Route 컴포넌트들 중에서 path prop이 /products인 Route만이 활성화 됩니다.
즉, Routes 컴포넌트 내부에서는 URL의 경로와 일치하는 "하나의 Route 컴포넌트만이 활성화"됩니다. 이는 react-router-dom v6부터 동작하는 내용이며 v6 이전에는 Switch 컴포넌트와 extact prop을 사용했어야 했습니다.

참고로 /a/:b/:c/:d처럼 경로 파라미터를 여러 개 설정할 수도 있습니다.

useParams 훅으로 경로 파라미터값 가져오기

우리는 경로 파라미터 값을 활성화된 Route 컴포넌트의 element prop에 작성된 페이지 컴포넌트가 취득하여 렌더링될 데이터를 결정해야 합니다.

react-router-dom의 "useParams 커스텀훅"을 통해서 동적으로 결정된 경로값, 즉 경로 파라미터의 값을 가져올 수 있습니다. useParams 훅은 객체를 반환하는데 그 객체에 프로퍼티 키로 콜론 뒤에 작성한 productId(경로 파라미터)가 있으며 프로퍼티 값으로는 동적으로 결정된 경로값이 설정되어 있습니다.

// 현재 URL은 "our-domain.com/products/p1"
import { useParams } from 'react-router-dom';

const ProductDetail = () => {
    // params 객체에는 :(콜론) 뒤에 작성된 경로 파라미터의 이름이 프로퍼티 키로 설정되고 
    // 동적으로 결정된 경로값이 프로퍼티 값으로 존재
    const params = useParams();  // -> { productId: 'p1' }
    
    return (
        <section>
            <h2>Product Detail Page</h2>
            <p>{params.productId}</p>
        </section>
    );
};

export default ProductDetail;

즉, 우리는 params 객체의 프로퍼티를 통해서 동적으로 결정된 경로값을 가져올 수 있으며, 이를 통해 어떤 데이터를 가져올 지 결정하여 화면에 렌더링할 수 있습니다.

중첩 라우팅

"중첩된 라우팅"이란 활성화된 페이지 컴포넌트 내에서 Route를 중첩하여 상위 Route와 하위 Route "동시에 활성화" 시켜주는 것입니다.
즉, 하나의 Route만 활성화하는 것이 아니라 "여러 Route를 동시에 활성화"하는 것입니다.

기본적으로 Routes 컴포넌트 내부에 작성된 Route 컴포넌트는 하나만 활성화 되기 때문에 만약 "여러 Route를 활성화"하고 싶다면 중첩 라우팅을 사용해야 합니다.

1. Route 컴포넌트 내 Route 컴포넌트 중첩

아래 예제에서는 our-domain/products에서는 Products 컴포넌트를 렌더링하고, our-domain/products/:productId일 때는 Products 컴포넌트와 ProductsDetail 컴포넌트 둘 다 렌더링하고자 합니다.

// App.js
import { Outlet, Routes } from 'react-router-dom';

const App = () => {
    return (
        // Routes 컴포넌트는 Route 컴포넌트와 Fragment만을 포함 가능
        <Routes>
            <Route path="/products" element={<Products />}>
                <Route path=":productId" element={<ProductDetail />} />
            </Route>
        </Routes>
    );
}

export default App;

Route 컴포넌트를 "중첩"하여 작성하였습니다. URL이 our-domain/products일 때는 Products 컴포넌트가 화면에 렌더링되고, URL이 our-domain/products/p1일 때는 Products 컴포넌트와 ProductDetail 컴포넌트가 화면에 둘 다 렌더링됩니다. 즉, Routes 컴포넌트 내부에서는 두 개의 Route가 활성화가 됩니다.

상위 Route의 element prop에 작성된 Products 컴포넌트 내부에서 중첩된 Route 컴포넌트의 element prop에 작성된 ProductDetail 컴포넌트가 렌더링될 위치를 지정할 수 있습니다.

// Products.js
import { Outlet } from 'react-router-dom';

const Products = () => {
    return (
        <>
            <h2>Products Page</h2>
            
            // Outlet 컴포넌트는 중첩하여 작성한 Route의 
            // element prop에 작성된 컴포넌트를 의미
            // 즉, ProductDetail 컴포넌트가 렌더링되는 위치
            <Outlet />
        </>
    );
}

export default Products;

Products 컴포넌트 내부에 작성된 Outlet 컴포넌트가 ProductDetail 컴포넌트가 됩니다. 즉, 상위 Route 컴포넌트에서 중첩된 Route 컴포넌트의 element prop에 작성된 컴포넌트는 "Outlet 컴포넌트"로 렌더링될 위치를 지정할 수 있습니다.

이때 주의할 점으로 중첩 라우팅을 위해 중첩시킨 Route의 path prop에는 절대 경로가 아닌 "상대 경로"를 작성해야 합니다. 이때 상대 경로의 기준은 상위 Route의 path prop값이 기준 URL 경로가 됩니다.

위 예제의 경우 중첩 Route의 path prop에 ":productId", 즉 상대 경로를 작성했습니다. 해당 중첩 Route는 상위 Route의 path prop 값인 "/proucts""/"":productId"이 뒤에 추가된, 즉 "/products/:productId"일 때 활성되됩니다.

이렇게 Route 컴포넌트를 여러 개 활성화 시키는 것이 가능합니다.

2. 컴포넌트 내부에서 Route 컴포넌트 중첩

중첩 라우팅을 하는 다른 방법도 존재합니다.

// App.js
// 현재 URL "our-domain.com/"
import { Outlet, Routes } from 'react-router-dom';

const App = () => {
    return (
        <Routes>
           // 경로가 "/products"로 시작하는 Route를 추가적으로 검색
           // 검색은 element prop에 작성한 컴포넌트 내부에서 추가적으로 Route 검색
            <Route path="/products/*" element={<Products />}>
        </Routes>
    );
}

export default App;

중첩 라우팅을 위해서 "상위 Route 컴포넌트의 path props 값의 마지막에 반드시 /*을 추가"해주어야 합니다. /*는 일치하는 Route 컴포넌트를 "추가적으로 검색"한다는 의미가 됩니다.

이때 추가적인 검색은 "element prop에 작성한 페이지 컴포넌트 내부"에서 Route 검색을 이어나갑니다.

Routes 컴포넌트내부에서는 현재 URL의 경로와 일치하는 Route 컴포넌트를 검색합니다. 일반적으로는 위에서부터 아래로 검색하여 일치하는 단 하나의 Route 컴포넌트를 검색합니다. 하지만 Route의 path prop에 작성된 값 마지막에 /*이 존재한다면 일단 해당 Route 컴포넌트를 활성화 시키고, element prop에 작성된 컴포넌트 내부에서 "추가적으로 검색"을 이어나갑니다.

// Products.js
// 현재 URL "our-domain.com/products"
import { Routes, Route } from 'react-router-dom';

const ProductDetail = () => {
    return (
        <>
            <h2>Products Page</h2>
            <Routes>
                // 상대 경로 작성시 현재 활성화된 Route의 path prop 값을 기준 경로
                // 즉, "/products/:productId"일 때 활성화
                <Route path=":productId" element={<ProductDetail />} />
            </Routes>
        </>
    );
}

export default ProductDetail;

그리고 Route의 element props에 작성한 Products 컴포넌트 내부에서 다시 Route 컴포넌트를 사용하여 중첩 라우팅을 할 수 있습니다. 이때 앞에서 설명한 방법과 동일하게 Route 컴포넌트의 path prop은 자동적으로 상대 경로로서 사용됩니다. 즉, 상위 Route 컴포넌트의 path prop에 작성된 값을 기준으로 하는 상대 경로가 결정됩니다.

중첩 라우팅 응용

중첩 라우팅으로 특정 URL 경로에서만 보여질 컴포넌트를 설정할 수도 있습니다.

// Products.js
// 현재 URL은 "our-domain.com/products"
import { Routes, Route } from 'react-router-dom';

const Products = () => {
    return (
        <>
            <h1>Products Page</h1>
            
            // "our-domain.com/products"일 때만 표시하고자 하는 엘리먼트
            <p>only products page</p>
            
            // 중첩 라우팅
            <Routes>
                <Route path="m1" element={<Book />} />
                <Route path="m2" element={<Car />} />
                <Route path="m3" element={<Computer />} />
            </Routes>
        </>
    );
};

export default Products;

Products 컴포넌트 내 중첩 라우팅을 사용하고 있습니다. 만약 URL이 "our-domain.com/products/m1"이라면 Routes 내 작성한 첫 번째 Route가 활성화되고 Book 컴포넌트가 렌더링됩니다. 이때 기존 Products 컴포넌트와 Book 컴포넌트 모두 렌더링됩니다.
그러므로 <p>only products page</p>또한 "our-domain.com/products/m1"일 때도 렌더링이 되지만 해당 엘리먼트를 "our-domain.com/products"일 때만 렌더링되도록 하기 위해서는 아래처럼 중첩 라우팅을 사용하여 구현할 수 있습니다.

// Products.js
// 현재 URL은 "our-domain.com/products"
import { Routes, Route } from 'react-router-dom';

const Products = () => {
    return (
        <>
            <h1>Products Page</h1>
            
            // 중첩 라우팅
            <Routes>
                // 오직 "our-domain.com/products"일 때만 활성화
                <Route path="" element={<p>only products page</p>} />
                
                // "our-domain.com/products/:productId"
                <Route path="m1" element={<Book />} />
                <Route path="m2" element={<Car />} />
                <Route path="m3" element={<Computer />} />
            </Routes>
        </>
    );
};

export default Products;

앞으로는 "our-domain.com/products/:productId"일 때 only products page라는 문구는 더이상 보이지 않게 됩니다. 해당 문구는 오직 "our-domain.com/products"일 때만 보여지게 됩니다.

react-router-dom v5에서 사용하던 Redirect 컴포넌트 대신 Navigate 컴포넌트를 사용합니다.

Navigate 컴포넌트를 사용하여 "URL 경로를 임의로 변경"하여 보여질 UI(컴포넌트)를 지정할 수 있습니다.

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

<Route path="경로" element={<Navigate replace to="변경될 경로" />} />

즉, 현재 URL의 경로가 Route의 path prop과 일치한다면 element prop에 작성된 Navigate 컴포넌트의 to prop에 작성된 경로로 현재 URL 경로를 변경합니다. 그리고 replace prop 작성시 현재 url 경로가 to prop에 작성한 값으로 대체(replace)됩니다.

// App.js
import { Routes, Route, Redirect } from 'react-router-dom';

const App = () => {
    return (
        <Routes>
            // 초기 경로인 "/"를 "/welcome"으로 변경
            <Route path="/" element={<Navigate replace to="/welcome" />} />
            
            <Route path="/welcome" element={<Welcome />} />
            <Route path="/products" element={<Products />} />
            <Route path="/products/:productid" element={<ProductDetail />} />
        </Routes>
    );
}

export default App;

페이지 초기에 갖게되는 URL 경로를 our-domain/대신 our-domain/welcome을 갖도록 설정합니다. 즉, 초기 URL이 our-domain/welcome을 갖게되어 path prop이 /welcome인 Route가 활성화 되어 Welcome 컴포넌트가 화면에 보이게 됩니다.

경로 이탈 페이지

Navigate 컴포넌트를 이용하여 Not Found Page를 구현할 수도 있습니다.

import { Routes, Route, Navigate } from 'react-router-dom;

cosnt App = () => {
    return (
        <Routes>
            ,,,
            
            // 가장 마지막에 Not found 페이지를 구현할 수 있음
            <Route path="*" element={<Navigate replace to="/welcome" />} />
        </Routes>
    );
};

export default App;

Routes 컴포넌트에 중첩된 Route 컴포넌트들은 위에서 아래로 현재 URL와 일치하는 Route 컴포넌트를 활성화 시킵니다. 이때 모든 Route 컴포넌트의 path prop와 일치하지 않는다면 위 코드처럼 "path prop에 *을 사용하여 어떤 URL 경로값이든 해당 Route 컴포넌트를 활성화" 시키도록 하고 element prop에 작성한 Navigate 컴포넌트를 통해서 원하는 URL로 임의로 변경하도록 작성할 수도 있습니다.

즉, *는 모든 경로를 의미하며 어떤 경로든 해당 Route가 활성화되도록 합니다. 앞에 작성한 모든 Route의 path prop의 경로와 일치하지 않는다면 마지막에 작성된 해당 Route를 활성화시킵니다. 가장 마지막에 작성해야 하는 이유로는 *는 모든 경로값과 일치하기 때문에 항상 Routes 내 마지막에 위치시켜야 합니다.

useNavigate 훅

react-router-dom의 useNavigate라는 커스텀 훅을 사용하여 현재 경로를 변경하는 로직을 작성할 수 있습니다.
react-router-dom v5까지는 useHistory라는 훅을 사용했지만 v6 부터는 "useNavigate"라는 훅을 사용합니다.

예를 들어, 어떤 제출 버튼을 클릭하면 데이터를 서버로 전달하고 다른 URL로 이동하려고 합니다. 이때 제출 버튼을 Link나 NavLink로 작성하는 것은 의미와 맞지 않기 때문에 우리는 제출 버튼을 사용하면서 경로를 변경하도록 만들어주어야 합니다. 이때 사용하는 것이 바로 useNavigate 훅입니다.

useNavigate 훅은 함수를 반환합니다. 그 함수에 전달하는 첫 번째 인수에 따라 이동할 경로가 결정됩니다.

두 번째 인수로 객체를 전달할 수 있습니다. 객체의 replace 프로퍼티에 true를 작성하면 현재 경로를 첫 번째 인수로 전달한 경로로 대체합니다. 즉, History 스택에 push되지 않고 replace로 동작합니다.

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

const navigate = useNavigate();

navigate('home');  // -> 상대 경로, 현재 경로의 끝에 인수로 전달한 경로를 추가(push)

navigate('/home'); // -> 절대 경로, 현재 경로를 인수로 변경(push)

navigate('/home', { replace: true }); // -> 절대 경로, 현재 경로를 첫 번째 인수로 대체(replace)

navigate(1); // -> 다음 경로(go)

navigate(-1); // -> 이전 경로(goback)

예를 들어, 현재 URL이 만약 "our-domain/products/product-detail"일 때

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

const navigate = useNavigate();

navigate('home');  // -> our-domain/products/product-detail/home

navigate('/home'); // -> our-domain/products/home

navigate(-1); // -> our-domain/products

navigate(1); // -> 앞으로 이동할 경로 존재하지 않음

첫 번째 인수로 전달한 값이 절대경로, 즉 "/"이 붙는다면 인수로 변경될 것이고, 상대경로, 즉 "/"을 선두에 제거한다면 끝에 추가될 것입니다. 그리고 인수로 인덱스를 나타내는 정수를 전달하여 이동할 수도 있습니다.

두 번째 인수로 전달한 객체의 replace 프로퍼티 값이 true인 경우 URL을 인수로 전달한 경로값으로 대체(replace)합니다. 참고로 두 번째 인수는 옵션으로 사용됩니다.

Prompt 컴포넌트

react-router-dom v6부터는 Prompt 컴포넌트를 지원하고 있지는 않지만 언젠간 추가될 것이라는 Github issue의 댓글이 존재하기 때문에 react-router-dom v5에서의 Prompt 컴포넌트의 사용법을 설명하겠습니다.

Prompt 컴포넌트는 react-router-dom에서 제공하는 컴포넌트로 경로가 변경되기 직전에 경고를 해줍니다. 즉, 어떤 조건이 충족이되고 경로를 변경하기 직전에 경고창을 사용자에게 제공하고 정말로 경로를 변경할 것인지 한 번 더 확인하는 것입니다.

이는 어떤 데이터를 제출하는 양식과 관련된 페이지에서 양식을 작성하다 사용자의 실수로 해당 경로를 벗어나는 경우가 존재할 수 있습니다. 만약 어떠한 경고도 해주지 않고 바로 해당 경로를 벗어나게 된다면 사용자가 작성하던 모든 데이터가 사라지게 됩니다. 이때 우리는 사용자에게 정말로 경로를 변경할 것인지 물어보도록 하는 작업을 하기 위해서 Prompt 컴포넌트를 사용할 수 있습니다.

// react-router-dom v5
import { Prompt } from 'react-router-dom';

<Prompt when={불리언값} message={location => { return '경고메세지';} } />

Prompt 컴포넌트는 when prop의 값이 true일 때만 해당 경로를 벗어나려고 할 때 경고창을 띄어줍니다. when이 false라면 해당 경로를 벗어난다고 해도 경고창을 띄우지 않습니다.

message prop에는 함수를 전달하는데 이때 함수는 location이라는 객체를 전달받고, 이 함수의 반환값으로 경고창의 메세지를 띄웁니다. location 객체는 추후에 설명하겠습니다.


// react-router-v5
import { useState } from 'react';

const Form = () => {
    const [isEntering, setIsEntering] = useState(false);
    
    const formSubmitHandler = event => {
        event.preventDefault();
        ,,,
    }
    
    const formFocusHandler = () => {
        setIsEntering(true);
    };
    
    const finishEnteredHandler = () => {
        setIsEntering(false);
    };
    
    return (
        <>
            <Prompt when={isEntered} message={(location) => 'Are you sure want to leave?';} />
            <form onFocus={formFocusHandler}>
                <label htmlFor='name'>name</label>
                <input type='text' id='name'/>
                <button onClick={finishEnteredHandler}>Add Name</button>
            </form>
    );
};

위 코드에서 폼이 포커스를 얻게되면 isEntering 상태값이 true로 변경되고, 이후 Prompt 컴포넌트로 인해 해당 url을 벗어나기 전에 경고창이 표시됩니다. 이때 Add Name 버튼을 눌렀을 때 url을 변경하는 것은 옳바른 동작이므로 finishEnteredHandler 이벤트 핸들러로 isEntering 상태값을 false로 변경시켜 줍니다.

만약 submit 이벤트 핸들러 내부에서 isEntering 상태를 변경을 하게되면 url을 변경하기 전에 isEntering 상태값을 false로 변경하지 못합니다. 즉, url이 변경된 이후에 isEntering 상태값이 변경되기 때문에 sumbit 이벤트 핸들러 내부에 작성해도 아무런 의미가 없습니다.

쿼리 파라미터 작업

쿼리 파라미터 추가

쿼리 파라미트는 URL의 "가장 마지막"에 작성됩니다. URL에 ?(물음표) 뒤에 매개변수 쌍이 &로 구분되어 전달되는 경우가 있는데 이는 로드된 페이지에 데이터를 추가로 전달해주는 역할을 합니다.

즉, 쿼리 파라미터의 경우 Route 매칭에 영향을 주지는 않지만 로드된 페이지의 "추가적인 정보"를 제공하기 위해서사용합니다.

우리는 useNavigate 훅이 반환하는 함수를 통해서 URL을 임의로 변경할 수 있었습니다. 이를 통해 쿼리 파라미터도 작성할 수 있습니다.

// 현재 URL이 "our-domain.com/"인 경우
const navigate = useNavigate();

// 현재 URL의 가장 마지막 부분에 인수로 전달한 값을 추가
// "our-domain.com/quotes?sort=asc"로 변경
navigate('quotes?sort=asc');

?뒤에 작성된 sort가 쿼리 파라미터(매개변수)가 되고, asc가 파리미터에 할당되는 값이 됩니다.

이렇게 useNavigate 훅이 반환한 함수를 통해 쿼리 파라미터를 추가할 수 있습니다.

쿼리 파라미터 추출

쿼리 파라미터가 갖는 값을 통해서 해당 URL로 변경될 때 페이지가 추가적인 동작을 하도록 하기 위해서는 쿼리 파라미터를 추출할 필요가 있습니다. 가져온 쿼리 파라미터 값을 통해 추가적인 동작을 설정할 수 있습니다.

우리는 react-router-dom의 "useLocation 이라는 훅을 사용하여 현재 페이지의 정보를 갖고 있는 객체(location)"를 사용할 수 있습니다. useLocation이 반환하는 location 객체는 URL이 변경될 때마다 새로운 location 객체를 반환합니다.

useLocation이 반환하는 객체는 다음과 같은 프로퍼티를 갖고 있습니다.

현재 URL이 아래와 같을 때 Location 객체의 프로퍼티는 다음과 같습니다.
our-domain/products/product-detial?sort=asc&id=p1#shoes

  • pathname: 현재 URL의 경로 부분(쿼리 파라미터 부분 제외한)
    ex)"/products/product-detail"

  • search: ?를 포함한 쿼리 파라미터부분
    ex)"?sort=asc&id=p1"

  • hash: URL의 #을 포함한 해시값
    ex)"#shoes"

  • state: 해당 URL로 변경될 때 전달될 값

  • key : location 객체의 식별값
    ex)"zwgcnjl3"


우리는 useLocation 훅이 반환한 location 객체의 search 파라미터를 통해서 쿼리 파라미터를 가져올 수 있습니다.

가져온 쿼리 파라미터는 문자열 형태이기 때문에 사용함에 있어서 불편함이 존재합니다. 그래서 우리는 "URLSearchParams라는 생성자 함수"를 사용여 쿼리 파라미터를 객체로 변환해줍니다. URLSearchParams는 Web API로서 제공되는 함수입니다.

인수로 쿼리 파라미터 문자열 값을 전달해주면 객체를 반환해줍니다. 이때 객체의 프로퍼티 키로 쿼리 파라미터 이름이 설정되고, 파라미터 값이 프로퍼티 값으로 설정됩니다.

즉, asc가 프로퍼티 키로 존재하고 그 값으로 asc가 존재합니다. 그리고 id라는 프로퍼티 키가 존재하고 그 값으로 p1이 존재하게 됩니다.

주의할 점으로 이때 프로퍼티 값을 그냥 참조하여 가져올 수 없고 생성된 객체로 "get 메서드"를 호출해야 합니다. 이때 인수로 가져올 값에 대응하는 프로퍼티 키를 인수로 전달해줍니다.

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

const loaction = useLoaciton();

const queryParams = new URLSearchParams(location.search);


queryParams.get('sort'); // -> 'asc'
queryParams.get('id');  // -> 'p1'
profile
Frontend Dev

0개의 댓글