TIL 22-04-30

thisisyjin·2022년 4월 30일
0

TIL 📝

목록 보기
33/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 13

  • 리액트 라우터로 SPA 개발

React-Router

라우팅(Routing) 과 SPA

Routing

  • 사용자가 요청한 URL에 따라 알맞은 페이지를 보여줌
  • 프로젝트를 하나의 페이지 or 여러 페이지로 구성 가능
    (사용 목적과 기능에 맞게)

라우팅 시스템

여러 페이지로 구성된 웹 어플리케이션을 만들 때,
페이지 별로 컴포넌트들을 분리해가며 프로젝트를 관리하기 위한 시스템.

  • ✅ React에서 라우팅 시스템을 구축하려면?
  1. React Router - 리액트 라우팅 관련 라이브러리. 컴포넌트 기반으로 라우팅 시스템 설정.
  2. Next.js- 리액트 프로젝트의 프레임워크(Framework).
    라우팅시스템, 최적화, 서버사이드 렌더링 등 다양한 기능 제공.
    파일 경로 기반으로 라우팅 시스템 설정.

-> 모두 서드파티로 제공되므로, 그 외에도 react-location 등이 존재.


SPA (Single Page Application)

  • 하나의 페이지로 이루어진 어플리케이션.
  • 싱글 라우터를 사용하여 여러 페이지로 구성된 어플리케이션을 제작할 수 있지만,
    이러한 멀티 페이지 어플리케이션은 사용자가 다른 페이지로 이동할 때 마다 새로운 html을 받아오고
    페이지 로딩할 때 마다 리소스들(css, js, image등)을 받아와야 했음.
    -> 각 페이지마다 다른 html을 만들어 제공하거나, 유동적인 html을 생성해주는 템플릿 엔진을 사용하기도 했음.

🙋‍♀️ Template Engine (템플릿 엔진)

  • 템플릿 엔진은 웹 템플릿들(web templates)과 컨텐츠 정보(content information)를 처리하기 위해 설계된 소프트웨어.
    = 응답으로 보내줄 웹페이지 모양을 미리 만들어 표준화한 것
  • HTML에 비해 간단한 문법을 사용함으로써 코드량을 줄일 수 있고, 데이터만 바뀌는 경우가 굉장히 많으므로 재사용성을 높일 수 있음.

예> pugejs 모듈

  • ejs 모듈 = 웹페이지를 동적으로 처리하는 템플릿 엔진 모듈. 특정 형식의 문자열을 HTML 문자열로 변환.
  • pug 모듈 = pug 모듈 또한 특정 형식의 문자열을 HTML 형식의 문자열로 변환 가능.
  • 사용자 인터렉션이 얼마 없는 경우 이전 방법대로 해도 좋지만, 인터렉션이 많고 다양한 정보를 제공하는 모던 웹은 이 방법이 효율적이지 않음.
    -> 새 페이지를 보여줄 때 마다 서버측에서 매번 준비를 해야 하므로 트래픽 증가.

🔻 해결 방법

  • 리액트 - 뷰(view) 렌더링을 사용자의 브라우저가 담당하게 함.
  • 사용자의 인터랙션 발생시 필요한 부분만 업데이트 하는 방식으로 사용
  • 만약 새로운 데이터가 필요시, 서버 API를 호출하여 데이터만 새로 불러와 사용 가능.

위와 같이 html을 한번만 받아와서 실행시킨 후, 그 이후에는 필요한 데이터만 받아와서 화면에 업데이트 하는것을 SPA라고 한다.

  • 기술적으론 한 페이지지만, 사용자에게는 여러 페이지로 느껴질 수 있음.
  • 실제로 주소를 변경해 서버에 페이지를 요청하는 것이 아닌,
    브라우저의 history API로 주소창의 값만 변경하고 서버요청은 ❌.

리액트 라우터 사용법

1. 프로젝트 생성 및 react-router-dom 설치

  • creact-react-app 설정
$ yarn create react-app router-dom-prac
  • react-router-dom 설치
$ yarn add react-router-dom 

2. BrowserRouter 적용

index.js

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

참고 - ReactDOM.render()React18 부터 이제 더이상 지원하지 않으므로,
root.render()안에 컴포넌트를 넣어주면 된다.

3. 페이지 제작

  • 각 페이지에 해당하는 컴포넌트 제작.

src/pages/Home.js

const Home = () => {
    return (
        <div>
            <h1></h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
        </div>
    )
}

export default Home;

src/pages/About.js

const About = () => {
    return (
        <div>
            <h1>소개</h1>
            <p>리액트 라우터 사용 프로젝트입니다.</p>
        </div>
    )
}

4. Route 컴포넌트 사용

  • react-router-domdml Route 컴포넌트로 각 컴포넌트를 보여줌.
  • Route들은 반드시 Routes로 감싸줘야 함.

App.js

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

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  )
}

export default App;

Route 컴포넌트는 App.js에서 사용한다.
가상의 주소로 이동하는 a태그 역할을 하는 Link 컴포넌트는 어디서든 사용 가능.

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

const Home = () => {
    return (
        <div>
            <h1></h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
            <Link to="/about">소개</Link>
        </div>
    )
}

export default Home;
  • react-router-dom의 Link 컴포넌트를 임포트하여 사용함.
  • Link 컴포넌트는 결국엔 anchor 태그로 나타나지만, 서버요청을 하는 링크 이동이 아닌 (=a태그)
    history API 를 이용해 브라우저의 주소 경로만 변경함.
<Link to="/about">소개</Link>

참고 - 지난 학습에서는 Create-react-app이 아닌 환경에서 직접 해서 그런지 몰라도
리로드시 (서버요청) -> 에러가 발생했었다.
Link를 통해서만 페이지 이동이 가능했었는데, 지금은 주소를 직접 입력해서 변경해도 잘 동작한다.
-> 아마 기본 설정차이인것 같은데, 서버에서 해당 path를 알게 하는 세팅이 되어있는듯?


URL 파라미터 / 쿼리스트링

페이지 주소 정의시, 동적인 값을 사용해야 할 때가 있음.
예> /profile/velopert

+) 쿼리스트링(query string) = /articles?page=1&keyword=react

URL 파라미터쿼리스트링
주소 경로에 동적인 값을 넣음.? 이후key=value 형태로 정의, &로 구분.
주로 id나 이름으로 특정 데이터 조회시 사용주로 검색, 정렬 등 데이터 조회 옵션 전달시 사용

URL 파라미터

useParams 사용

src/pages/Profile.js

import { useParams } from "react-router-dom";

const data = {
    velopert: {
        name: '김민준',
        desc: '리액트를 좋아하는 개발자',
    },
    gildong: {
        name: '홍길동',
        desc: '고전 소설 주인공',
    },
};

const Profile = () => {
    const params = useParams();
    const profile = data[params.username];

    return (
        <div>
            <h1>사용자 프로필</h1>
            {profile ? (
                <div>
                    <h2>{profile.name}</h2>
                    <p>{profile.desc}</p>
                </div>

            ) : (
                <p>존재하지 않는 프로필</p>
            )}
        </div>
    )
}

export default Profile;
  • data에서 params.username 프로퍼티에 해당하는 값의 객체를 불러오고,
    그 객체(=profile)의 정보들을 렌더링하는 컴포넌트임.
  • URL 파라미터의 이름은 route 설정을 할 때 Route 컴포넌트의 path를 통해 설정함.
    -> App.js에서 Route를 추가하여 URL 파라미터를 등록.

Route 추가

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

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path='/profiles/:username' element={<Profile />} />
    </Routes>
  )
}

export default App;
  • URL 파라미터는 경로에 :를 사용하여 설정함.
  • 만약 여러개인 경우, /profiles/:username/:field 와 같이 설정 가능.

src/pages/Home.js

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

const Home = () => {
    return (
        <div>
            <h1></h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
            <ul>
                <li><Link to="/about">소개</Link></li>
                <li><Link to="/profiles/velopert">velopert의 프로필</Link></li>
                <li><Link to="/profiles/gildong">gildong의 프로필</Link></li>
                <li><Link to="/profiles/none">존재하지 않는 프로필</Link></li>
            </ul>
        </div>
    )
}

export default Home;
  • 가독성을 위해 ul>li 구조 안에 넣어줌.


(URL 파라미터를 보면, 잘 변경되고 있음.)


쿼리스트링

  • 쿼리스트링 사용시 Route 컴포넌트 사용시에도 별도 설정이 필요함.
  • 우선, 쿼리스트링을 먼저 띄워야 함.

About.js

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;
  • useLocation은 location 객체를 반환.
  • location 객체는 사용자가 현재 보고 있는 페이지의 정보를 지님.
  • pathname: 현재 주소의 경로 (쿼리스트링 제외)
  • search: ?문자를 포함한 쿼리스트링 값
  • hash: # 뒤의 값 (history API 지원 안하는 구형 브라우저에서 클라이언트 라우팅시 해시 라우터에서 사용)
  • state: 페이지로 이동할 때 임의로 넣는 상태 값
  • key: location 객체의 고유 값. 초기에는 default이며, 페이지가 변경될 때 마다 고유 값 생성.
  • 쿼리스트링은 location.search 를 통해 조회할 수 있음.
    (자바스크립트 내장 함수임.)
  • 주소창에 /about?detail=true&mode=1 을 입력하면 아래와 같이 쿼리스트링이 나온다.
  • 이 쿼리스트링 ?detail=true&mode=1에서 ?를 지우고, &로 구분하는 작업이 필요하다.
  • 보통은 qs 또는 querystring 패키지를 설치해 처리 가능하다.
  • 리액트 라우터 V6부터는 useSearchParams 라는 Hook을 통해 쿼리스트링을 파싱 할 수 있다!

useSearchParams

About.js

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>
    )
}

export default About;
  • useSearchParams()배열을 반환하며,
  • 첫번째 요소는 쿼리파라미터를 조회하거나 수정하는 메서드가 담긴 객체를 반환. (없으면 null)
  • 두번째 요소는 쿼리파라미터를 객체 형태로 업데이트 할 수 있는 함수를 반환.

❗️ 주의

  • 쿼리파라미터는 무조건 문자열 타입이다. true나 false도 문자열로 'true'와 같이 조회해야 하고,
    숫자의 경우에는 parseInt를 해서 비교해야 한다.

  • searchParams를 변경하려면 setSearchParams()로만 가능.
  • searchParams.get('detail'), searchParams.get('mode')를 해서
    searchParams는 {detail: '~', mode: '~'}의 형태가 된다.

✅ 참고 - searchParams.get()

https://example.com/?name=Jonathan&age=18 일때, get() 함수 사용 예.

let name = params.get("name");   //  "Jonathan"
let age = parseInt(params.get("age"), 10);   //  18

mdn - 참고 문서


중첩된 라우트

  • 중첩된 라우트의 경우

Articles.js

import { Link } from "react-router-dom";

const Articles = () => {
    return (
        <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>
    )
}

export default Articles;

Article.js

import { useParams } from "react-router-dom";

const Article = () => {
    const { id } = useParams();
    return (
        <div>
            <h2>게시글 {id}</h2>
        </div>
    )
}

export default Article;

App.js

  • Route 추가.
import { Route, Routes } from 'react-router-dom';
import About from './pages/About'; 
import Home from './pages/Home'; 
import Profile from './pages/Profile';
import Article from './pages/Article';
import Articles from './pages/Articles';

function App() {
  return (
    <Routes> 
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path='/profiles/:username' element={<Profile />} />
      <Route path='/articles' element={<Articles />} />
      <Route path='/article:id' element={<Article />} />
    </Routes>
  )
}

export default App;

그리고 Home.js에 Articles로 가는 Link 컴포넌트를 추가.

...
<li><Link to="/articles">게시글 목록</Link></li>
...

중첩된 라우트 방식

만약 중첩된 라우트 방식으로 작성하면?

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

function App() {
  return (
    <Routes> 
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path='/profiles/:username' element={<Profile />} />
      <Route path='/articles' element={<Articles />}>
        <Route path=':id' element={<Article />} />
      </Route>
    </Routes>
  )
}

export default App;

Outlet 컴포넌트 사용

Articles.js

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;

  • <Outlet/> 컴포넌트는 Route의 children으로 들어가는 JSX 컴포넌트를 보여주는 역할을 함.
  • 여기서는 Route path=":id" element={<Article />} /> 가 보여짐.
    -> 중첩되어 Route path="articles"의 자식으로 들어간 것.

공통 레이아웃 컴포넌트

중첩 라우트 + Outlet은 페이지끼리 공통적으로 보여줘야 하는 레이아웃에도 유용.
예> Home, About, Profile에 공통적으로 상단에 헤더를 보여줘야 할 경우.

Header 컴포넌트 생성 후 각 페이지에서 재사용 하는 방법도 OK.
중첩 라우트와 Outlet을 활용하여 구현할 수 있음.
-> 한번만 사용하면 됨.

1. 공통 레이아웃을 위한 Layout 컴포넌트 생성.

Layout.js

import { Outlet } from "react-router-dom";

const Layout = () => {

    return (
        <div>
            <header style={{background: 'lightgray', padding: 16, fontSize: 24}}>
                Header
            </header>
            <main>
                <Outlet/>
            </main>
        </div>
    )
}
export default Layout;

2. App 컴포넌트 수정

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

function App() {
  return (
    <Routes> 
      <Route element={<Layout />}>
        <Route path="/" 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>
    </Routes>
  )
}

export default App;
  • <Route element={<Layout />}> 로 Home, About, Profile을 감싸준다.

Layout 컴포넌트에서 Outlet은 children 컴포넌트들인 Home, About, Profile 이다.


index props

  • Route 컴포넌트에는 index 라는 props가 존재.
  • 이 props는 path="/" 와 동일한 의미를 가짐.
    -> Home 컴포넌트를 의미.
...
<Route index element={<Home />} />
...

index를 사용하면, 상위 라우트의 경로와 일치하지만
그 이후 경로가 주어지지 않을 때 보여지는 라우트 설정 가능.


리액트 라우터 부가 기능

1) useNavigate

  • Link 컴포넌트를 사용하지 않ㄱ도 다른 페이지로 이동해야 하는 경우.

Layout.js

import { Outlet, useNavigate } from "react-router-dom";

const Layout = () => {
    const navigate = useNavigate();

    const goBack = () => {
        navigate(-1);
        // 이전 페이지로 이동
    };

    const goArticles = () => {
        navigate('/articles');
        // articles 경로로 이동 
    }
    return (
        <div>
            <header style={{ background: 'lightgray', padding: 16, fontSize: 24 }}>
                <button onClick={goBack}>뒤로 가기</button>
                <button onClick={goArticles}>게시글 목록</button>
            </header>
            <main>
                <Outlet />
            </main>
        </div>
    )
}
export default Layout;
  • navigate(-1)을 하면 뒤로 한 번 간다.
  • 반대로, navigate(1)을 하면 앞으로 한 번 간다.

{ replace : true }

  • 다른 페이지로 이동 시 replace 라는 옵션을 준다면, 페이지 이동시 현재 페이지를 기록하지 않음.
navigate('/articles', { replace: true });

navigate 함수의 첫번째 인자로는 path를 주고, 두번째 인자로는 옵션을 주었다.


  • 링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 - 특정 스타일 or CSS 클래스 적용.
<NavLink 
  style={({isActive}) => isActive ? activeStyle : undefined}
/>
  • Articles 컴포넌트 수정
import { NavLink, Outlet } from "react-router-dom";

const Articles = () => {
    const activeStyle = {
        color: 'green',
        fontSize: 21,
    }

    return (
        <div>
            <Outlet />
            <ul>
                <li>
                    <NavLink to="/articles/1" style={({isActive}) => (isActive ? activeStyle : undefined)}>게시글 1</NavLink>
                </li>
                <li>
                    <NavLink to="/articles/2" style={({isActive}) => (isActive ? activeStyle : undefined)}>게시글 2</NavLink>
                </li>
                <li>
                    <NavLink to="/articles/3" style={({isActive}) => (isActive ? activeStyle : undefined)}>게시글 3</NavLink>
                </li>
            </ul>
        </div>
       
    )
}

export default Articles;

  • 위와 같이 현재 보고 있는 게시글의 링크에 스타일이 적용됨.

  • 반복되는 코드가 여러번 사용되고 있으므로,
    이런 경우에는 현재 NavLink를 감싼 컴포넌트를 생성하여 사용하는 것이 좋음. (리팩토링)

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;

3) NotFound 페이지

  1. src/pages/NotFound.js
const NotFound = () => {
    return (
        <div style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: 64,
            position: 'absolute',
            width: '100%',
            height: '100%',
        }}>
            404
        </div>
    )
}

export default NotFound;
  1. App.js (수정)
...

function App() {
  return (
    <Routes> 
      <Route 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>
  )
}

export default App;
  • *(wildcard 문자)를 이용하여 모든 텍스트에 매칭함.
  • 상단 Route들에 해당하지 않는다면 이 라우트가 화면에 나타남. (조건문의 else 같은 느낌)

  • 임의의 존재하지 않는 주소 입력시, NotFound 컴포넌트가 출력된다.

4) Navigate 컴포넌트

  • 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동하고 싶을 때 사용.
    -> 리다이렉트를 하고 싶을 때.

📌 리다이렉트란?

= redirect(다시 지시하다)
브라우저가 /page1 URL을 웹 서버에 요청 -> 서버는 HTTP 응답 메시지를 통해 /page2로 다시 요청하라고 브라우저에게 다른 URL을 지시.

🙋‍♂️ 예시 - 로그인을 한 회원만 볼 수 있는 마이페이지에 로그인 하지 않은 사람이 마이페이지 url로 접속하려 하면, 권한이 없기 때문에 로그인 페이지나 메인 페이지로 리다이렉트 걸어준다.

1. Login 컴포넌트 생성

const Login = () => {
    return (
        <div>
            <input placeholder="ID" />
            <input type="password" placeholder="PW" />
            <button type="submit">로그인</button>
        </div>
    );
}

export default Login;

2. MyPage 컴포넌트 생성

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;
  • isLoggedIn 값이 false면 리다이렉션이 되도록 설정.
  • /login 경로로 이동하도록.
  • replace 옵션은 useNavigate에서 사용 했던 것과 동일.
    = 히스토리에 남기지 않도록 하는 것.

3. App.js 수정

...
function App() {
  return (
    <Routes> 
      <Route 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="/login" element={<Login />} />
      <Route path="/mypage" element={<MyPage />} />
      <Route path='*' element={<NotFound />} />
    </Routes>
  )
}
export default App;

  • /mypage를 입력하면 로딩되는 순간 바로 Login 페이지로 이동된다.
    (주소창 주목!)
  • 지금 만든 프로젝트는 About 페이지에 접속시 당장 필요하지 않은 Profile, Articles 컴포넌트의 코드까지 불러옴.
  • 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지로 이동하면 효율적.
    -> 코드 스플리팅기술로 해결 가능.
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글