Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 13
- 리액트 라우터로 SPA 개발
라우팅 시스템
여러 페이지로 구성된 웹 어플리케이션을 만들 때,
페이지 별로 컴포넌트들을 분리해가며 프로젝트를 관리하기 위한 시스템.
React Router - 리액트 라우팅 관련 라이브러리. 컴포넌트 기반으로 라우팅 시스템 설정.Next.js- 리액트 프로젝트의 프레임워크(Framework).-> 모두 서드파티로 제공되므로, 그 외에도 react-location 등이 존재.
멀티 페이지 어플리케이션은 사용자가 다른 페이지로 이동할 때 마다 새로운 html을 받아오고🙋♀️ Template Engine (템플릿 엔진)
- 템플릿 엔진은 웹 템플릿들(web templates)과 컨텐츠 정보(content information)를 처리하기 위해 설계된 소프트웨어.
= 응답으로 보내줄 웹페이지 모양을 미리 만들어 표준화한 것- HTML에 비해 간단한 문법을 사용함으로써 코드량을 줄일 수 있고, 데이터만 바뀌는 경우가 굉장히 많으므로 재사용성을 높일 수 있음.
예>
pug와ejs모듈
- ejs 모듈 = 웹페이지를 동적으로 처리하는 템플릿 엔진 모듈. 특정 형식의 문자열을 HTML 문자열로 변환.
- pug 모듈 = pug 모듈 또한 특정 형식의 문자열을 HTML 형식의 문자열로 변환 가능.
위와 같이 html을 한번만 받아와서 실행시킨 후, 그 이후에는 필요한 데이터만 받아와서 화면에 업데이트 하는것을 SPA라고 한다.
- 기술적으론 한 페이지지만, 사용자에게는 여러 페이지로 느껴질 수 있음.
- 실제로 주소를 변경해 서버에 페이지를 요청하는 것이 아닌,
브라우저의 history API로 주소창의 값만 변경하고 서버요청은 ❌.
react-router-dom 설치$ yarn create react-app router-dom-prac$ yarn add react-router-dom 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()안에 컴포넌트를 넣어주면 된다.
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>
    )
}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;Link 컴포넌트를 임포트하여 사용함.history API 를 이용해 브라우저의 주소 경로만 변경함.<Link to="/about">소개</Link>참고 - 지난 학습에서는 Create-react-app이 아닌 환경에서 직접 해서 그런지 몰라도
리로드시 (서버요청) -> 에러가 발생했었다.
Link를 통해서만 페이지 이동이 가능했었는데, 지금은 주소를 직접 입력해서 변경해도 잘 동작한다.
-> 아마 기본 설정차이인것 같은데, 서버에서 해당 path를 알게 하는 세팅이 되어있는듯?
페이지 주소 정의시, 동적인 값을 사용해야 할 때가 있음.
예> /profile/velopert
+) 쿼리스트링(query string) = /articles?page=1&keyword=react
| URL 파라미터 | 쿼리스트링 | 
|---|---|
| 주소 경로에 동적인 값을 넣음. | ? 이후에 key=value 형태로 정의, &로 구분. | 
| 주로 id나 이름으로 특정 데이터 조회시 사용 | 주로 검색, 정렬 등 데이터 조회 옵션 전달시 사용 | 
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;params.username 프로퍼티에 해당하는 값의 객체를 불러오고,path를 통해 설정함.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;:를 사용하여 설정함.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;
(URL 파라미터를 보면, 잘 변경되고 있음.)
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;
- 쿼리스트링은
location.search를 통해 조회할 수 있음.
(자바스크립트 내장 함수임.)- 주소창에 /about?detail=true&mode=1 을 입력하면 아래와 같이 쿼리스트링이 나온다.
?detail=true&mode=1에서 ?를 지우고, &로 구분하는 작업이 필요하다.useSearchParams 라는 Hook을 통해 쿼리스트링을 파싱 할 수 있다!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()는 배열을 반환하며, ❗️ 주의
- 쿼리파라미터는 무조건 문자열 타입이다. true나 false도 문자열로 'true'와 같이 조회해야 하고,
숫자의 경우에는 parseInt를 해서 비교해야 한다.

✅ 참고 - searchParams.get()
https://example.com/?name=Jonathan&age=18일때, get() 함수 사용 예.let name = params.get("name"); // "Jonathan" let age = parseInt(params.get("age"), 10); // 18
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
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;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을 활용하여 구현할 수 있음.
-> 한번만 사용하면 됨.
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;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가 존재.path="/" 와 동일한 의미를 가짐....
<Route index element={<Home />} />
...index를 사용하면, 상위 라우트의 경로와 일치하지만
그 이후 경로가 주어지지 않을 때 보여지는 라우트 설정 가능.
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;replace 라는 옵션을 준다면, 페이지 이동시 현재 페이지를 기록하지 않음.navigate('/articles', { replace: true });navigate 함수의 첫번째 인자로는 path를 주고, 두번째 인자로는 옵션을 주었다.
<NavLink 
  style={({isActive}) => isActive ? activeStyle : undefined}
/>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;const NotFound = () => {
    return (
        <div style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: 64,
            position: 'absolute',
            width: '100%',
            height: '100%',
        }}>
            404
        </div>
    )
}
export default NotFound;...
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 문자)를 이용하여 모든 텍스트에 매칭함.
리다이렉트를 하고 싶을 때.📌 리다이렉트란?
= redirect(다시 지시하다)
브라우저가 /page1 URL을 웹 서버에 요청 -> 서버는 HTTP 응답 메시지를 통해 /page2로 다시 요청하라고 브라우저에게 다른 URL을 지시.🙋♂️ 예시 - 로그인을 한 회원만 볼 수 있는 마이페이지에 로그인 하지 않은 사람이 마이페이지 url로 접속하려 하면, 권한이 없기 때문에 로그인 페이지나 메인 페이지로 리다이렉트 걸어준다.
const Login = () => {
    return (
        <div>
            <input placeholder="ID" />
            <input type="password" placeholder="PW" />
            <button type="submit">로그인</button>
        </div>
    );
}
export default Login;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;replace 옵션은 useNavigate에서 사용 했던 것과 동일....
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;
- 지금 만든 프로젝트는 About 페이지에 접속시 당장 필요하지 않은 Profile, Articles 컴포넌트의 코드까지 불러옴.
- 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지로 이동하면 효율적.
->코드 스플리팅기술로 해결 가능.