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 컴포넌트의 코드까지 불러옴.
- 라우트에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지로 이동하면 효율적.
->코드 스플리팅
기술로 해결 가능.