- 리액트에서는 웹사이트도 컴포넌트로 만들 수 있음
- 리액트 라우터(React Router) 사용
1. 리액트 라우터 v6 소개
리액트 라우터
- 리액트 컴포넌트로 페이지를 나누고 이동하게 해주는 라이브러리
- 페이지 나누기
- 페이지 이동
- 핵심 컴포넌트
-> 라우터(Router), 라우츠(Routes), 라우트(Route), 링크 (link)
라우터 (Rounter)
- 리액트 라우터에서 사용하는 데이터들을 모두 갖고 있음
- 현재 주소, 페이지 기록같은 데이터를 가지고 있음
- Router 컴포넌트도 내부적으로는 context provider임
라우츠(Routes), 라우트(Route)
- Routes 컴포넌트 안에서 Route 컴포넌트로 페이지의 경로랑 보여줄 컴포넌트를 지정
링크(link)
- 리액트 라우터에서 a 태그 대신에 사용하는 것
2. 리액트 라우터 설치하기
1) 패키지
- Node.js를 설치하면 npm(노드 패키지 매니저)라는 프로그램도 함께 설치
- npm은 패키지를 설치하거나 삭제하는 것처럼 패키지를 관리하는 프로그램
2) 패키지 설치하기
- 패키지를 설치하려면 package.json이 있는 폴더에서 터미널을 열고 npm install <패키지 이름>이라는 명령어를 실행
- npm install react-router-dom@6
3) 라우터 컴포넌트 감싸기
- 프로젝트의 최상위 컴포넌트인 Main.js 파일에 가서 라우터 컴포넌트 적용
- BrowserRouter라는 컴포넌트를 불러와서 컴포넌트 전체를 감싸줌
import { BrowserRouter } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
function Main() {
return (
<BrowserRouter>
<App>
<HomePage />
</App>
</BrowserRouter>
);
}
export default Main;
3. Routes로 페이지 나누기
- Routes 컴포넌트는 여러 개의 Route를 포함
- Routes를 렌더링할 때 리액트 라우터는 Routes 안에 잇는 Route를 차례대로 검사하면서 현재경로가 path와 일치하는지 하나씩 검사
- 일치하는 경로를 찾으면 element프롭으로 지정한 컴포넌트를 렌더링 해줌
- Route 컴포넌트의 path 프롭으로 경로를 지정하고 element 프롭으로 보여줄 컴포넌트를 지정
- element 프롭은 컴포넌트 함수가 아니라 jsx를 넘겨줌
Main.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
import CoursePage from './pages/CoursePage';
import CourseListPage from './pages/CourseListPage';
import WishlistPage from './pages/WishlistPage';
function Main() {
return (
<BrowserRouter>
<App>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="courses" element={<CourseListPage />} />
<Route
path="courses/react-frontend-development"
element={<CoursePage />}
/>
<Route path="wishlist" element={<WishlistPage />} />
</Routes>
</App>
</BrowserRouter>
);
}
export default Main;
4. Link로 이동하기
- Link 컴포넌트에서는 to라는 프롭을 사용하여 이동할 경로를 지정
- '/'를 맨 앞에다가 붙이는 건 절대 경로라는 의미
Nav.js
import Container from './Container';
import UserMenu from './UserMenu';
import logoImg from '../assets/logo.svg';
import styles from './Nav.module.css';
function Nav() {
return (
<div className={styles.nav}>
<Container className={styles.container}>
<Link to="/">
<img src={logoImg} alt="Codethat Logo" />
</Link>
<ul className={styles.menu}>
<li><Link to="/courses">카탈로그</Link></li>
<li><Link to="/questions">커뮤니티</Link></li>
<li>
<UserMenu />
</li>
</ul>
</Container>
</div>
);
}
export default Nav;
5. NavLink로 네비게이션 구현하기
- 내브링크(NavLink)라는 컴포넌트를 사용해서 선택된 메뉴 강조 밑줄 기능 구현
- NavLink는 스타일(style)이라는 프롭으로 함수 지정 가능
- 현재 페이지의 경로가 내비게이션의 링크에 해당하면 isActive값이 참이 됨
Nav.js
import { Link, NavLink } from 'react-router-dom';
import Container from './Container';
import UserMenu from './UserMenu';
import logoImg from '../assets/logo.svg';
import styles from './Nav.module.css';
function getLinkStyle({ isActive }) {
return {
textDecoration: isActive ? 'underline' : '',
};
}
function Nav() {
return (
<div className={styles.nav}>
<Container className={styles.container}>
<Link to="/">
<img src={logoImg} alt="Codethat Logo" />
</Link>
<ul className={styles.menu}>
<li>
<NavLink style={getLinkStyle} to="/courses">
카탈로그
</NavLink>
</li>
<li>
<NavLink style={getLinkStyle} to="/questions">
커뮤니티
</NavLink>
</li>
<li>
<UserMenu />
</li>
</ul>
</Container>
</div>
);
}
export default Nav;
6. 하위 페이지 나누기
- 코스 목록 페이지랑 코스 페이지를 하위 Route로 만들기
- 리액트 라우터는 Route를 중첩해서 사용할 수 있게 해줌
- 하위 경로에서 index에 해당하는 Route에서는 path 대신에 index라는 프롭을 사용
- App 컴포넌트는 공통 레이아웃을 렌더링하는 컴포넌트
- 하위 Route 여러 개에서 공통된 디자인을 보여주고 싶을 때는 element 프롭을 지정하고 지정된 컴포넌트에서는 Outlet 컴포넌트를 사용하면 됨
-> Routes 컴포넌트 안에선느 반드시 Route만 사용해야 하기 때문
Main.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
import CoursePage from './pages/CoursePage';
import CourseListPage from './pages/CourseListPage';
import QuestionPage from './pages/QuestionPage';
import QuestionListPage from './pages/QuestionListPage';
import WishlistPage from './pages/WishlistPage';
function Main() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<HomePage />} />
<Route path="courses">
<Route index element={<CourseListPage />} />
<Route path="react-frontend-development" element={<CoursePage />} />
</Route>
<Route path="questions" element={<QuestionListPage />} />
<Route path="questions/616825" element={<QuestionPage />} />
<Route path="wishlist" element={<WishlistPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default Main;
App.js
import { Outlet } from 'react-router-dom';
import Nav from '../components/Nav';
import Footer from '../components/Footer';
import styles from './App.module.css';
import './App.font.css';
function App() {
return (
<>
<Nav className={styles.nav} />
<div className={styles.body}><Outlet /></div>
<Footer className={styles.footer} />
</>
);
}
export default App;
7. useParams로 동적인 경로 만들기
- 코스 상세 페이지를 동적인 경로로 바꾸기
- 동적인 경로를 지정하려면 경로에 ':'으로 시작하는 단어를 사용하면 됨
- courseSlug라는 변수로 페이지의 경로 받아오기
- 경로에서 사용하는 동적인 값을 파라미터라고 부름
- 이러한 파라미터를 모아 놓은 것을 params라고 부름
- useParams라는 리액트 라우터에서 제공하는 커스텀 훅을 사용
- useParams가 리턴하는 객체에는 현재 경로의 파라미터들이 저장되어 있음
-> 이 객체에 우리가 정의한 courseSlug라는 값도 저장되어 있음
Main.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
import CoursePage from './pages/CoursePage';
import CourseListPage from './pages/CourseListPage';
import QuestionPage from './pages/QuestionPage';
import QuestionListPage from './pages/QuestionListPage';
import WishlistPage from './pages/WishlistPage';
function Main() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<HomePage />} />
<Route path="courses">
<Route index element={<CourseListPage />} />
<Route path=":courseSlug" element={<CoursePage />} />
</Route>
<Route path="questions">
<Route index element={<QuestionListPage />} />
<Route path="616825" element={<QuestionPage />} />
</Route>
<Route path="wishlist" element={<WishlistPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default Main;
CoursePage.js
import Button from '../components/Button';
import { useParams } from 'react-router-dom';
import { addWishlist, getCourseBySlug } from '../api';
import Container from '../components/Container';
import Card from '../components/Card';
import CourseIcon from '../components/CourseIcon';
import getCourseColor from '../utils/getCourseColor';
import styles from './CoursePage.module.css';
function CoursePage() {
const { courseSlug } = useParams();
const course = getCourseBySlug(courseSlug);
const courseColor = getCourseColor(course?.code);
const headerStyle = {
borderTopColor: courseColor,
};
const handleAddWishlistClick = () => {
addWishlist(course?.slug);
};
return (
<>
<div className={styles.header} style={headerStyle}>
<Container className={styles.content}>
<CourseIcon photoUrl={course.photoUrl} />
<h1 className={styles.title}>{course.title}</h1>
<Button variant="round" onClick={handleAddWishlistClick}>
+ 코스 담기
</Button>
<p className={styles.summary}>{course.summary}</p>
</Container>
</div>
<Container className={styles.topics}>
{course.topics.map(({ topic }) => (
<Card className={styles.topic} key={topic.slug}>
<h3 className={styles.title}>{topic.title}</h3>
<p className={styles.summary}>{topic.summary}</p>
</Card>
))}
</Container>
</>
);
}
export default CoursePage;
8. 없는 페이지 처리하기
- 웹 서핑을 하다보면 페이지가 없어졌거나 주소를 잘못 쳐서 없는 주소로 들어가는 경우
- 경로를 찾을 수 없을 때 보여줄 페이지 지정
- Routes 안에서 차례대로 Route를 검사하다가 일치하는 path가 없으니까 빈화면 보여주게됨
-> 모든 경로를 포함하는 Route를 추가
- 잘못된 경로로 접속하면 일치하는 Route가 없을 테니까 차례대로 검사하다가 맨 마지막에 있는 Route에서 NotFoundPage를 보여줌
Main.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './components/App';
import HomePage from './pages/HomePage';
import CoursePage from './pages/CoursePage';
import CourseListPage from './pages/CourseListPage';
import QuestionPage from './pages/QuestionPage';
import QuestionListPage from './pages/QuestionListPage';
import WishlistPage from './pages/WishlistPage';
import NotFoundPage from './pages/NotFoundPage';
function Main() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<HomePage />} />
<Route path="courses">
<Route index element={<CourseListPage />} />
<Route path=":courseSlug" element={<CoursePage />} />
</Route>
<Route path="questions">
<Route index element={<QuestionListPage />} />
<Route path=":questionId" element={<QuestionPage />} />
</Route>
<Route path="wishlist" element={<WishlistPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default Main;
NotFoundPage.js
import { Link } from 'react-router-dom';
import Button from '../components/Button';
import Container from '../components/Container';
import Warn from '../components/Warn';
import styles from './NotFoundPage.module.css';
function NotFoundPage() {
return (
<Container className={styles.container}>
<Warn
variant="big"
title="존재하지 않는 페이지에요."
description="올바른 주소가 맞는지 다시 한 번 확인해 주세요."
/>
<div className={styles.link}>
<Link to="/">
<Button as="div">홈으로 가기</Button>
</Link>
</div>
</Container>
);
}
export default NotFoundPage;
9. Navigate로 리다이렉트 하기
리다이렉트 (Redirect)
- 페이지에 접속했을 떄 어떤 이유 때문에 다른 페이지로 이동
- 코스 목록 페이지로 리다이렉트
- CoursePage를 렌더링할 때 course의 값이 없으면 Navigate라는 컴포넌트르 리턴
-> 렌더링하면 to 프롭에 지정된 경로로 이동시켜 줌
CoursePage.js
...
function CoursePage() {
const { courseSlug } = useParams();
const course = getCourseBySlug(courseSlug);
const courseColor = getCourseColor(course?.code);
if (!course) {
return <Navigate to="/courses" />;
}
...
10. useSearchParams로 쿼리 사용하기
- 쿼리 스트링을 받아서 검색어 기능 만들기
- 검색어를 입력하고 엔터를 입력하거나 검색 버튼을 누르면 페이지 주소 뒤에 "?keyword=검색어"를 붙여서 이동하고
페이지에서는 검색된 결과를 보여줌
쿼리 스트링(Query String)
- 주소에서 추가적인 조건을 넣을 때 사용
- form 태그의 기본동작은 엔터나 submit 버튼을 누르면 쿼리와 함께 페이지를 이동
- input 태그에는 name값을 "keyword"라고 지정
- handleKeywordChange라는 함수가 있어서 값을 입력할 때마다 keyword State값이 함께 변경됨
<form className={searchBarStyles.form} onSubmit={handleSubmit}>
<input
name="keyword"
value={keyword}
onChange={handleKeywordChange}
placeholder="검색으로 코스 찾기"
></input>
<button type="submit">
<img src={searchIcon} alt="검색" />
</button>
</form>
- 리액트 라우터에서는 쿼리 파라미터값을 가져오고 싶을 때 useSearchParms라는 훅을 사용
- useSearchParms는 serachParams와 setter 함수를 배열형으로 리턴
- searchParams에서는 get()이라는 함수로 값을 가져올 수 있음
- setter 함수인 setSearchParams()에 값을 지정하려면, 객체를 넘겨주면 됨
- keyword state 값은 인풋에 value로 내려주기 때문에 initKeyword 값이 없는 경우에는 빈문자열로 지정
- handleSubmit() 함수 구현
-> 폼 태그의 기본 동작을 막은 다음에, setSearchParams()라는 setter함수를 이용해서 쿼리 값을 변경
CourseListPage.js
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import ListPage from '../components/ListPage';
import Warn from '../components/Warn';
import CourseItem from '../components/CourseItem';
import { getCourses } from '../api';
import styles from './CourseListPage.module.css';
import searchBarStyles from '../components/SearchBar.module.css';
import searchIcon from '../assets/search.svg';
function CourseListPage() {
const [searchParam, setSearchParam] = useSearchParams();
const initKeyword = searchParam.get('keyword');
const [keyword, setKeyword] = useState(initKeyword || '');
const courses = getCourses(initKeyword);
const handleKeywordChange = (e) => setKeyword(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
setSearchParam(keyword ? { keyword } : {});
};
return (
<ListPage
variant="catalog"
title="모든 코스"
description="자체 제작된 코스들로 기초를 쌓으세요."
>
<form className={searchBarStyles.form} onSubmit={handleSubmit}>
<input
name="keyword"
value={keyword}
onChange={handleKeywordChange}
placeholder="검색으로 코스 찾기"
></input>
<button type="submit">
<img src={searchIcon} alt="검색" />
</button>
</form>
<p className={styles.count}>총 {courses.length}개 코스</p>
{initKeyword && courses.length === 0 ? (
<Warn
className={styles.emptyList}
title="조건에 맞는 코스가 없어요."
description="올바른 검색어가 맞는지 다시 한 번 확인해 주세요."
/>
) : (
<div className={styles.courseList}>
{courses.map((course) => (
<CourseItem key={course.id} course={course} />
))}
</div>
)}
</ListPage>
);
}
11. useNavigate로 페이지 이동하기
- 코드를 통해 페이지를 이동
- 코스 담기 버튼을 눌렀을 때 실행할 함수
-> useNavigate라는 커스텀 훅을 사용
CoursePage.js
const handleAddWishlistClick = () => {
addWishlist(course?.slug);
navigate('/wishlist');
};
12. 리액트 라우터 정리
1) 리액트 라우터란?
- 리액트에서 경로에 따라 페이지를 나누도록 해주는 라이브러리
- 컴포넌트를 사용해서 페이지를 나눔
2) 라우터
- 리액트 라우터를 사용하려면 반드시 라우터라는 컴포넌트가 필요
- BrowserRouter를 사용
->이 컴포넌트를 최상위 컴포넌트에서 감싸주면 모든것에서 사용할 수 있음
import { BrowserRouter } from 'react-router-dom';
function App() {
return <BrowserRouter> ... </BrowserRouter>;
}
3) 페이지를 나누는 방법
- Routes 컴포넌트 안에다가 Route 컴포넌트를 배치해서 각 페이지를 나눠줄 수 있음
- 이때 Routes 안에서는 위에서부터 차례대로 Route를 검사
- 현재 경로와 path prop이 일치하는 Route를 찾음
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="posts" element={<PostListPage /> />
<Route path="posts/1" element={<PostPage/>} />
</Routes>
4) 링크
- 리액트 라우터에서는 <a> 태그 대신에 Link 컴포넌트를 사용
- to 라는 prop으로 이동할 경로를 정해주면 됨
<Link to="/posts">블로그</Link>
5) 하위 페이지 나누기
- Route 컴포넌트에다가 Route 컴포넌트를 배치하면 됨
- 이때 하위 페이지에서 최상위 경로에 해당하는 경로는 path prop이 아니라 index라는 prop을 사용하면 됨
<Routes>
<Route path="/"><HomePage /></Route>
<Route path="posts" element={<PostLayout />} >
<Route index element={<PostListPage />} />
<Route path="1" element={<PostPage />} />
</Route>
</Routes>
- 부모 Route 컴포넌트에 element를 지정하고, Outlet 이라는 컴포넌트를 활용하면 공통된 레이아웃을 지정해 줄 수 있음
import { Outlet } from 'react-router-dom';
function PostLayout() {
return (
<div>
<h1>블로그</h1>
<hr />
<Outlet />
</div>
);
}
export default PostLayout;
6) 동적인 경로 다루기
- 콜론(:)으로 시작하는 문자열을 사용하면 경로에 파라미터를 지정할 수 있음
ex)
/posts/:postId라는 경로는 /posts/123 이라던지 /posts/abc라는 주소로 접속하면 123이나 abc 라는 값을 postId라는 파라미터로 받음
<Routes>
<Route path="/"><HomePage /></Route>
<Route path="posts" element={<PostLayout />} >
<Route index element={<PostListPage />} />
<Route path=":postId" element={<PostPage />} />
</Route>
</Routes>
- 경로 파라미터를 사용하려면 useParams 라는 훅을 사용하면 됨
function PostPage() {
const { postId } = useParams();
// ...
}
7) 쿼리 사용하기
- useSearchParams라는 Custom hook으로 SearchParams 객체를 받아올 수 있음
-> SearchParms 객체와 Setter 함수를 배열형으로 리턴
-> 이때 쿼리 값은 SearchParams의 get 함수로 가져옴
import { useSearchParams } from 'react-router-dom';
function PostListPage() {
const [searchParams, setSearchParams] = useSearchParams();
const filterQuery = searchParams.get('filter');
// ...
}
- 쿼리 값을 변경하고 주소를 이동하고 싶다면 Setter 함수에 객체를 넘겨주면 됨
- 이때 객체의 프로퍼티로 쿼리 값을 지정할 수 있음
ex) ?filter=react라는 쿼리로 이동
setSearchParams({
filter: 'react',
});
8) 페이지 이동하기
Navigate 컴포넌트
- 리턴 값으로 Navigate 컴포넌트를 리턴하면 to prop으로 지정한 경로로 이동
function PostPage() {
// ...
const post = getPost(postId);
// post가 없는 경우 /posts 페이지로 이동
if (!post) {
return <Navigate to="/posts" />;
}
// ...
}
useNavigate Hook
- useNavigate 라는 hook으로 navigate 함수를 가져오면 이 함수를 통해 페이지를 이동할 수 있음
const navigate = useNavigate();
const handleClick = () => {
// ... 어떤 작업을 한 다음에 페이지를 이동
navigate('/wishlist');
}
9) Link, Navigate, useNavigate를 사용하는 예시 상황
Link
- 사용자가 클릭해서 페이지를 이동하도록 할 때 사용
- 하이퍼링크 텍스트나 페이지를 이동하는 버튼, 이미지 등에 사용
Navigate
- 특정 경로에서 렌더링 시점에 다른 페이지로 이동시키고 싶을 때 사용
ex)
- 쇼핑몰의 회원 전용 페이지에 로그인 없이 들어와서 로그인 페이지로 리다이렉트 하는 경우
- 쇼핑몰의 상품 상세 페이지에서 제품이 품절되었거나 삭제되어서 다른 페이지로 이동시키는 경우
useNavigate
- 특정한 코드의 실행이 끝나고 나서 페이지를 이동시키고 싶을 때 사용
ex)
- 쇼핑몰에서 장바구니에 담기를 눌렀을 때 리퀘스트를 보내고 장바구니 페이지로 이동시키는 경우
- 쇼핑몰에서 결제하기 버튼을 누르고 나서 모든 결제가 완료된 후에 페이지를 이동시키는 경우
- 리다이렉트된 로그인 페이지에서 로그인을 완료한 후에 처음 진입했던 페이지로 돌아가는 경우
13. 싱글 페이지 애플리케이션(SPA) 이해하기
클라이언트사이드 렌더링(Clinet- side Rendering)
- 클라이언트 = 웹 브라우저
- 렌더링 = HTML 페이지를 만드는 것
- 웹 브라우저에서 자바스크립트로 HTML 페이지를 만듬
싱글 페이지 애플리케이션 (single Page Application)
- 싱글 페이지 = 하나의 HTML문서
- 애플리케이션 = 마치 앱처럼 여러 페이지를 돌아다니는 사이트
- 여러 경로의 HTML 문서를 돌아다니는 게 아니라 하나의 HTML 문서 안에서 자바스크립트로 여러 페이지를 보여줌
- root div에다가 클라이언트사이드 렌더링을 하는데 리액트 라우터가 주소창에 적힌 경로를 읽어서 경로에 일치하는 컴포넌트르 렌더링하는 방식
14. 리액트를 렌더링하는 방식
1) 대표적인 렌더링의 종류
클라이언트사이드 렌더링(Client-side Rendering)
- 리액트로 할 수 있는 가장 기본적인 방식의 렌더링
- 리액트로 작성한 코드는 자바스크립트로 변환 가능
- 자바스크립트로 변환된 리액트 코드를 웹 브라우저에서 실행해서 HTML을 만드는 것
서버사이드 렌더링(Server-side Rendering)
- 서버에서 HTML을 만들고 리스폰스로 보내주는 것
- 백엔드 서버에서 리퀘스트를 받으면 상황에 맞는 HTML을 만들어서 리스폰스로 보내주는 방식
- 이미 렌더링된 것이 웹 브라우저에 도착하니까 훨씬 빨리 화면을 띄어줄 수 있고, 검색 엔진에서 좋은 점수를 받아서 검색했을 때 사이트가 잘 노출될 수 있음
정적 사이트 생성 (Static Site Generation)
- 미리 HTML 파일을 만들어서 서버를 배포한느 것
2) 렌더링을 활용한 리액트 기술
Next.js
- 리액트 서버사이드 렌더링을 편하게
- 리액트 라우터랑은 다르게 HTML 파일을 나누듯이 자바스크립트 파일을 나눠 놓으면 곧바로 페이지로 사용할 수 있는 장점
Gatsby
- 리액트로 정적 사이트 만들기
- 리액트 코드를 미리 렌더링 해서 프로젝트를 빌드할 때 HTML 파일로 만듬
- 리액트로 만든사이트를 빌드해서 손쉽게 HTML 파일로 만들 수 있음
React Native
- 모바일 앱의 화면도 리액트로
- 리액트로 작성한 코드를 모바일 앱으로 만들 수 있게 해줌
- 리액트 코드로 개발하면 웹과 안드로이드와 iOS앱에서 사용하는 공통적인 코들르 한 번에 개발할 수 있음
'react' 카테고리의 다른 글
[소플] ch 5. 컴포넌트와 Props (0) | 2023.10.27 |
---|---|
[소플] ch4. 엘리먼트 렌더링 (1) | 2023.10.27 |
[코드잇] 리액트로 데이터 다루기(5) (0) | 2023.09.25 |
[코드잇] 리액트로 데이터 다루기 (4) (0) | 2023.09.24 |
[코드잇] 리액트로 데이터 다루기(3) (0) | 2023.09.04 |