라우터를 사용하기 위해서 우선 아래 패키지를 설치한다.
npm i react-router-dom@6.22.3
나중에 또 보고 싶은 글만 모은 리스트 페이지를 만들어보자
1. 카드 컴포넌트에 체크박스(토글버튼) 추가
체크박스가 체크되어 있다면 isFavoriteListed 값은 True 반환.
토글버튼을 클릭하면 onChange 이벤트 핸들러가 호출되면서 블로그 아이디 값을 매개변수로 하는 toggleFavoriteList 함수 호출
<label className="switch">
<input type="checkbox" checked={isFavoriteListed} onChange={() => toggleFavoriteList(blog.id)}></input>
<span className="slider">
<span className="slider-label">{isFavoriteListed ? "In List" : "Add to List"}</span>
</span>
</label>
2. 리팩토링 진행
블로그 글을 fetch하고 상태 관리하는 부분을 App.js로 이동시켜 전역적으로 블로그 글 데이터를 사용할 수 있도록 함.
찜 목록을 만들기 위해 찜목록의 상태를 관리하는 useState 추가.
찜 목록에 없다면 추가하고 있다면 제외하는 토글 함수(toggleFavoriteList) 로직 추가
// App.js에 추가
// 찜목록 만들기
const [favoriteList, setFavoriteList] = useState([]);
// favoriteList를 업데이트 진행
// 만약 현재 리스트에 블로그 아이디가 존재한다면 제외처리 없다면 추가
const toggleFavoriteList = (blogId) => {
setFavoriteList(prev =>
prev.includes(blogId) ? prev.filter(id => id !== blogId) : [...prev, blogId]
)
}
2. Router를 이용해서 페이지 경로 분리
참고로 페이지 분리 후, 각 페이지별로 블로그 글 목록, 찜 목록, 토글 함수를 인자값으로 넘김
<Router>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/favoriteList">Favorite List</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<BlogGrid blogs={blogs} favoriteList={favoriteList} toggleFavoriteList={toggleFavoriteList} />}></Route>
<Route path="/favoriteList" element={<FavoriteList favoriteList={favoriteList} blogs={blogs} toggleFavoriteList={toggleFavoriteList} />}></Route>
</Routes>
</Router>
3. 글 목록 페이지 수정
BlogGrid.js에서 필터링 된 블러그 값에 대해서 for문을 돌며 글 정보, 토글 함수, 찜 여부를 넘긴다
filteredBlogs.map(blog => (
<BlogCard blog={blog}
key={blog.id}
toggleFavoriteList={toggleFavoriteList}
isFavoriteListed={favoriteList.includes(blog.id)}></BlogCard>
))
4. 찜 목록 페이지 수정
import React from "react";
import "../styles.css";
import BlogCard from "./BlogCard";
export default function FavoriteList({ blogs, favoriteList, toggleFavoriteList }) {
return (
<div>
<h1 className="title">My Favorite List</h1>
<div className="watchlist">
{
favoriteList.map(id => {
const blog = blogs.find(blog => blog.id === id)
return <BlogCard key={id} blog={blog} toggleFavoriteList={toggleFavoriteList} isFavoriteListed={true}></BlogCard>
})
}
</div>
</div>
)
}
5. 결과
토글된 결과를 보여주는 페이지
참고로 이번 웹페이지 제작 실습은 DB를 연결하지 않았기 때문에 useState가 App.js에서 관리되는 등 일반적인 웹페이지 제작과는 다른 형태로 개발되었다. 향후 DB가 연결된다면 다른 형태로 설계될 것이다.
최종 코드
// App.js
import './App.css';
import './styles.css';
import Header from './components/Header';
import Footer from './components/Footer';
import BlogGrid from './components/BlogGrid';
import FavoriteList from './components/FavoriteList';
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"
import React, { useState, useEffect } from 'react';
function App() {
// 블로그 상태를 업데이트 한다
const [blogs, setBlogs] = useState([]);
// 찜목록 만들기
const [favoriteList, setFavoriteList] = useState([]);
// favoriteList를 업데이트 진행
// 만약 현재 리스트에 블로그 아이디가 존재한다면 제외처리 없다면 추가
const toggleFavoriteList = (blogId) => {
setFavoriteList(prev =>
prev.includes(blogId) ? prev.filter(id => id !== blogId) : [...prev, blogId]
)
}
// fetch로 데이터를 가져온 후 json으로 변환 후 blogs 배열을 업데이트 한다.
useEffect(() => {
fetch("blog.json")
.then(response => response.json())
.then(data => setBlogs(data))
}, [])
return (
<div className="App">
<div className='container'>
<Header></Header>
<Router>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/favoriteList">Favorite List</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<BlogGrid blogs={blogs} favoriteList={favoriteList} toggleFavoriteList={toggleFavoriteList} />}></Route>
<Route path="/favoriteList" element={<FavoriteList favoriteList={favoriteList} blogs={blogs} toggleFavoriteList={toggleFavoriteList} />}></Route>
</Routes>
</Router>
</div>
<Footer></Footer>
</div>
);
}
export default App;
// BlogCard.js
import React from "react";
import "../styles.css"
// 이미지를 못 불러올 경우 예외 처리
const handleError = (e) => {
e.target.src = "images/default.jpg"
}
// 평점에 따른 UI 변경
const getScoreClass = (score) => {
if (score > 8) return "score-good"
if (score >= 5 && score < 8) return "score-ok"
if (score < 5) return "score-bad"
}
export default function BlogCard({ blog, isFavoriteListed, toggleFavoriteList }) {
return (
<div key={blog.id} className='blog-card'>
<img src={blog.image} alt={blog.title} onError={handleError} />
<div className='blog-card-info'>
<h3 className='blog-card-title'>{blog.title}</h3>
<p className='blog-card-mood'>{blog.date}</p>
<div>
<span className='blog-card-mood'>{blog.mood}</span>
<span className={`blog-card-score ${getScoreClass(blog.score)}`}>{blog.score}</span>
</div>
<label className="switch">
<input type="checkbox" checked={isFavoriteListed} onChange={() => toggleFavoriteList(blog.id)}></input>
<span className="slider">
<span className="slider-label">{isFavoriteListed ? "In List" : "Add to List"}</span>
</span>
</label>
</div>
</div>
)
}
// BlogGrid.js
import React, { useState } from 'react';
import '../styles.css';
import BlogCard from './BlogCard';
export default function BlogGrid({ blogs, favoriteList, toggleFavoriteList }) {
// 검색용
const [searchTerm, setSearchTerm] = useState("");
// 기분 필터링용
const [mood, setMood] = useState("All Moods");
// 점수 필터링용
const [score, setScore] = useState("All");
const handleSearchChange = (e) => {
// 입력창의 값을 searchTerm 변수에 실시간 업데이트 진행
setSearchTerm(e.target.value)
}
// 콤보박스에서 mood 값 받기
const handleMoodChange = (e) => {
setMood(e.target.value)
}
// 콤보박스에서 평점 값 받기
const handleScoreChange = (e) => {
setScore(e.target.value)
}
// 감정 상태 필터링
const matchesMood = (blog, mood) => {
return mood === 'All Moods' || blog.mood.toLowerCase() === mood.toLowerCase();
}
// 영화 제목 필터링
const matchesSearchTerm = (blog, searchTerm) => {
return blog.title.toLowerCase().includes(searchTerm.toLowerCase())
}
// 영화 평점 필터링
const matchesScore = (blog, score) => {
let check_rating;
if (blog.score > 8) {
check_rating = 'Good'
} else if (blog.score <= 8 && blog.score >= 5) {
check_rating = 'Ok'
} else if (blog.score < 5) {
check_rating = 'Bad'
}
return score === 'All' || check_rating.toLowerCase() === score.toLowerCase()
}
// 블로그 필터링
const filteredBlogs = blogs.filter(blog =>
matchesMood(blog, mood) &&
matchesScore(blog, score) &&
matchesSearchTerm(blog, searchTerm)
)
return (
<div>
<input type="text"
className='search-input'
placeholder="Search Blogs..."
value={searchTerm}
onChange={handleSearchChange}
/>
<div className='filter-bar'>
<div className='filter-slot'>
<label>기분</label>
<select className='filter-dropdown' value={mood} onChange={handleMoodChange}>
<option>All Moods</option>
<option>happy</option>
<option>sad</option>
<option>tired</option>
<option>angry</option>
<option>anxious</option>
<option>neutral</option>
</select>
</div>
<div className='filter-slot'>
<label>점수</label>
<select className='filter-dropdown' value={score} onChange={handleScoreChange}>
<option>All</option>
<option>Good</option>
<option>Ok</option>
<option>Bad</option>
</select>
</div>
</div>
<div className='blogs-grid'>
{
filteredBlogs.map(blog => (
<BlogCard blog={blog}
key={blog.id}
toggleFavoriteList={toggleFavoriteList}
isFavoriteListed={favoriteList.includes(blog.id)}></BlogCard>
))
}
</div>
</div>
)
}
[참고] Develop React JS web applications including components, state, effect, hooks, React router, reducer, context, etc.