해당 포스트는 CodeStates 과정중의 내용을 복습하며 정리한 내용입니다.
이번 과제는 이번 유닛에서 배운 React Hooks
를 적용해보고, Custom Hook
과 React.lazy()
와 Suspense
를 이용하여 React 앱을 직접 리팩토링을 해봅니다.
어떤 부분을 Custom Hook으로 만들 수 있을 것인지, 어떻게 React.lazy()
와 Suspense
를 적용해볼 수 있을 것인지 페어와 함께 고민하며 앱을 리팩토링 해보세요.
먼저 터미널을 열어 npm을 이용해 전역 설치를 해주자.
npm i -g json-server
그 다음에 sprint의 data에 들어가서 다음 명령어를 입력하자.
json-server --watch data.json --port 3001
App.js에서는 react.lazy()
와 suspense
를 사용하여 컴포넌트를 리팩토링하였다.
json 파일이 있는 data 폴더에서 서버를 실행시키고 json 파일을 이용하여
get,post 요청시 그에 맞게 응답해주는 형식이다.
먼저 app.js 에서 Route 설정한 컴포넌트를 lazy
로 바꾸고 Suspense
로 감싼다.
Suspense
, lazy
를 import
해주는 것을 잊지말자.
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { Suspense, lazy, useEffect, useState } from 'react';
import useFetch from './util/useFetch'
const Home = lazy(() => import('./Home'));
const Navbar = lazy(() => import('./component/Navbar'));
const CreateBlog = lazy(() => import('./blogComponent/CreateBlog'));
const BlogDetails = lazy(() => import('./blogComponent/BlogDetail'));
const NotFound = lazy(() => import('./component/NotFound'));
const Footer = lazy(() => import('./component/Footer'));
const Loading = lazy(() => import('./component/Loading'))
function App() {
const { blogs, isPending, error, } = useFetch(`http://localhost:3001/blogs/`)
return (
<BrowserRouter>
{error && <div>{error}</div>}
<Suspense fallback={<Loading />}>
<div className="app">
<Navbar />
<div className="content">
<Routes>
<Route exact path="/" element={<Home blogs={blogs} isPending={isPending} />} />
<Route path="/create" element={<CreateBlog />} />
<Route path="/blogs/:id" element={<BlogDetails />} />
{/* 이렇게 path에 *(와일드카드)를 넣으면 매치되는 URL이 없을 때 해당 컴포넌트를 보여줍니다. */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<Footer />
</div>
</Suspense>
</BrowserRouter>
);
}
export default App;
현재는 개별 블로그 내용으로 진입해도 내용이 보이지 않기 때문에 useParams
로 작성한 내용의 id
값을 받아와 해당 id
내용만 화면에 띄우도록한다.
delete
버튼을 누르면 다시 home으로 리다이렉트되도록 useNavigate()
를 이용하여 로직을 작성하였고,
하트를 누르면 home에서 새로고침을 했을 때 숫자가 올라가도록 만들어 주었다.
작성글의 세부내용을 볼 때 스크롤이 최상단으로 위치할 수 있도록 useScroll을 작성했는데, 이 부분이 Hook이라고 할 수 있는지 의문이다.
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import useFetch from "../util/useFetch";
export const useScroll = (e) => {
return window.scrollTo(0, 0)
}
const BlogDetails = () => {
const { id } = useParams();
const [isLike, setIsLike] = useState(true);
const { blogs: blog, isPending, error } = useFetch(`http://localhost:3001/blogs/${id}`);
const navigate = useNavigate();
useScroll()
const handleDeleteClick = () => {
/* delete 버튼을 누르면 다시 home으로 리다이렉트 되어야 합니다. */
/* useNavigate()를 이용하여 로직을 작성해주세요. */
fetch(`http://localhost:3001/blogs/${id}`, {
method: "DELETE",
headers: {
'Content-Type': 'application/json'
},
})
.then(() => {
navigate('/')
window.location.reload()
})
.catch(err => console.log(err))
console.log('delete!');
}
const handleLikeClick = () => {
/* 하트를 누르면 home에서 새로고침을 했을 때 숫자가 올라가야 합니다. */
/* isLike와 blog.likes를 이용하여 handleLikeClick의 로직을 작성해주세요. */
setIsLike(!isLike);
let likeUpdate = blog.likes
isLike === false ? (likeUpdate > 0 ? likeUpdate = blog.likes - 1 : likeUpdate = blog.likes)
: likeUpdate = blog.likes + 1
const putData = {
'id': blog.id,
"title": blog.title,
"body": blog.body,
"author": blog.author,
"likes": likeUpdate,
}
console.log(likeUpdate)
fetch(`http://localhost:3001/blogs/${id}`, {
method: "PUT",
body: JSON.stringify(putData),
headers: {
'Content-Type': 'application/json'
},
})
.then(() => {
navigate(`/blogs/${blog.id}`)
})
.catch(err => console.log(err))
console.log('like!');
}
const counstLike = () => {
console.log(isLike)
}
return (
<div className="blog-details" >
{isPending && <div>Loading...</div>}
{error && <div>{error}</div>}
{blog && (
<article>
<h2>{blog.title}</h2>
<p>Written by {blog.author}</p>
<div>{blog.body}</div>
<button onClick={handleLikeClick}>
{/* isLike에 의해 조건부 렌더링으로 빨간 하트(❤️)와 하얀 하트(🤍)가 번갈아 보여야 합니다. */}
{!isLike ? '❤️' : '🤍'}
</button>
<button onClick={handleDeleteClick}>delete</button>
<button onClick={counstLike}></button>
</article>
)}
</div>
);
}
export default BlogDetails;
CreateBlog.js에서는 앞에서와 같이 등록 버튼을 누르면 게시물이 등록이 되며 home으로 리다이렉트 되도록 useNavigate()
를 이용하여 작성 하였다.
useInput이라는 Hook을 작성하여 입력 값에 대한 State를 컨트롤 했다.
input
/textarea
/select
태그까지 custom하고 싶었는데 다시 도전해봐야겠다.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useInput from "../util/useInput";
const CreateBlog = () => {
const [title, onChangeText] = useInput('');
const [body, onChangeBody] = useInput('');
const [author, onChangeAuthor] = useInput('김코딩');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
console.log(e.type);
/* 등록 버튼을 누르면 게시물이 등록이 되며 home으로 리다이렉트 되어야 합니다. */
/* 작성한 내용과 useNavigate를 이용하여 handleSubmit의 로직을 작성해보세요. */
const data = { title, body, author, likes: 0 }
fetch('http://localhost:3001/blogs/', {
method: "POST",
headers: { "Content-type": "Application/json" },
body: JSON.stringify(data)
})
.then(res => {
if (!res.ok) {
throw Error('could not fetch the data for that resource', {
method: "DELETE"
});
}
return res.json();
})
.then(() => {
navigate('/')
window.location.reload();
})
.catch(err => {
console.error("Error", err);
})
}
return (
<div className="create">
<h2>Add a New Blog</h2>
<form onSubmit={handleSubmit}>
<label>제목</label>
<input
type="text"
required
value={title}
onChange={onChangeText}
placeholder="제목을 입력해주세요."
/>
<label>내용</label>
<textarea
required
value={body}
onChange={onChangeBody}
placeholder="내용을 입력해주세요."
></textarea>
<label>작성자</label>
<select
value={author}
onChange={onChangeAuthor}
>
<option value="김코딩">김코딩</option>
<option value="박해커">박해커</option>
</select>
<button>등록</button>
</form>
</div>
);
}
export default CreateBlog;
CreateBlog.js 에서 사용한 Custom Hook
import { useState } from "react";
const useInput = (intialValue = "") => {
const [text, setText] = useState(intialValue);
const onChangeText = e => setText(e.target.value);
return [text, onChangeText, setText];
}
export default useInput
GET 메소드를 통해 데이터를 받아오는 useEffect hook은 컴포넌트 내 여기저기 존재하고 있었지 때문에 UseFetch라는 Custom Hook을 작성해 주었다.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
/* useState를 이용하여 data, isPending, error를 정의하세요. */
const [blogs, setBlogs] = useState(null);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState(null);
/* useFetch 안의 중심 로직을 작성해주세요. */
useEffect(() => {
setTimeout(() => {
fetch(url)
.then(res => {
if (!res.ok) {
throw Error('could not fetch the data for that resource');
}
return res.json();
})
.then(data => {
setIsPending(false);
setBlogs(data)
setError(null)
})
.catch(err => {
setIsPending(false);
setError(err.message)
})
}, 1000)
}, [])
return { blogs, isPending, error, }
}
export default useFetch;