Jieunny์ ๋ธ๋ก๊ทธ
S4) Unit 4. [์ค์ต] Custom Hook ๋ณธ๋ฌธ
๐ฃ React Custom Hook ์ ์ฌ์ฉํด์ ๋ธ๋ก๊ทธ ํ๋ฉด ๊ตฌํํด๋ณด๊ธฐ
๐ญ. ๊ตฌํ ์กฐ๊ฑด
1๏ธโฃ Bare Minimum
- App ๋ฃจํธ ์ปดํฌ๋ํธ(App.js)
- react.lazy()์ suspense๋ฅผ ์ฌ์ฉํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌํฉํ ๋งํฉ๋๋ค.
- BlogDetail ์ปดํฌ๋ํธ(BlogDetail.js)
- ํ์ฌ๋ ๊ฐ๋ณ ๋ธ๋ก๊ทธ ๋ด์ฉ์ผ๋ก ์ง์
ํด๋ ๋ด์ฉ์ด ๋ณด์ด์ง ์์ต๋๋ค.
- useParams()์ ์ด์ฉํ์ฌ ๊ฐ๋ณ id๋ฅผ ๋ฐ์์ ๊ฐ๋ณ ๋ธ๋ก๊ทธ์ ๋ด์ฉ์ด ๋ณด์ผ ์ ์๋๋ก ํด๋ด ๋๋ค.
- delete ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ค์ home์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋์ด์ผ ํฉ๋๋ค.
- useNavigate()๋ฅผ ์ด์ฉํ์ฌ handleDeleteClick ํจ์์ ๋ก์ง์ ์์ฑํด์ฃผ์ธ์.
- ํํธ๋ฅผ ๋๋ฅด๋ฉด home์์ ์๋ก๊ณ ์นจ์ ํ์ ๋ ์ซ์๊ฐ ์ฌ๋ผ๊ฐ์ผ ํฉ๋๋ค.
- isLike์ blog.likes๋ฅผ ์ด์ฉํ์ฌ handleLikeClickํจ์์ ๋ก์ง์ ์์ฑํด์ฃผ์ธ์.
- isLike์ ์ํด ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ผ๋ก ๋นจ๊ฐ ํํธ(โค๏ธ)์ ํ์ ํํธ(๐ค)๊ฐ ๋ฒ๊ฐ์ ๋ณด์ฌ์ผ ํฉ๋๋ค.
- ํ์ฌ๋ ๊ฐ๋ณ ๋ธ๋ก๊ทธ ๋ด์ฉ์ผ๋ก ์ง์
ํด๋ ๋ด์ฉ์ด ๋ณด์ด์ง ์์ต๋๋ค.
- CreateBlog ์ปดํฌ๋ํธ(CreateBlog.js)
- ๋ฑ๋ก ๋ฒํผ์ ๋๋ฅด๋ฉด ๊ฒ์๋ฌผ์ด ๋ฑ๋ก์ด ๋๋ฉฐ home์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋์ด์ผ ํฉ๋๋ค.
- fetch์ useNavigate๋ฅผ ์ด์ฉํ์ฌ handleSubmit ์ด๋ฒคํธ๋ฅผ ์์ฑํด๋ด ๋๋ค.
- UseFetch ์ปดํฌ๋ํธ(UseFetch.js)
- GET ๋ฉ์๋๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ useEffect hook์ ์ปดํฌ๋ํธ ๋ด ์ฌ๊ธฐ์ ๊ธฐ ์กด์ฌํ๊ณ ์์ต๋๋ค.
- ํด๋น hook์ ๋ฐ๋ณต์ด ๋๋ ๋ถ๋ถ์ด ์์ผ๋ฏ๋ก ์ด๋ป๊ฒ custom hook์ผ๋ก ๋ง๋ค ์ ์์ ์ง ๊ณ ๋ฏผํด๋ด ์๋ค.
- util ํด๋ ๋ด์ ์กด์ฌํ๋ useFetch์ custom hook์ ์์ฑํด์ฃผ์ธ์.
- useState๋ฅผ ์ด์ฉํ์ฌ data, isPending, error๋ฅผ ์ ์ํ์ธ์.
- useFetch ์์ ์ค์ฌ ๋ก์ง์ ์์ฑํด์ฃผ์ธ์.
2๏ธโฃ Advanced
- ๋ธ๋ก๊ทธ ๊ธ ํด๋ฆญํด์ ๋ค์ด๊ฐ์ ๋ ์คํฌ๋กค ๋งจ ์๋ก ์ ์ฉ๋๋ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
- ์ ์ฉ๋ ์ปดํฌ๋ํธ ์ง์ ์ ํ์ด์ง ๋งจ ์๋ก ์คํฌ๋กคํด์ฃผ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณผ ์ ์์ต๋๋ค.
- ์ด ๊ธฐ๋ฅ ๋ํ useEffect๋ฅผ ์ด์ฉํด ๊ตฌํํ ์ ์์ผ๋ฏ๋ก, custom hook์ผ๋ก ๋ง๋ค๊ธฐ ์ ์ ํฉ๋๋ค.
- ์ด๋ป๊ฒ ๊ตฌํํ ์ ์์ ์ง ํ์ด์ ํจ๊ป ๊ณ ๋ฏผํ๊ณ custom hook์ผ๋ก ๋ง๋ค์ด๋ด ์๋ค.
- CreateBlog ์ปดํฌ๋ํธ(CreateBlog.js)
- ํ์ฌ CreateBlog ์ปดํฌ๋ํธ ๋ด form ๋ด input๊ณผ textarea, select๋ ์ ๋ถ ํ๋์ฝ๋ฉ์ด ๋์ด ์์ต๋๋ค.
- input๊ณผ textarea, select๋ ์ ๋ถ custom hook์ผ๋ก ๋ฆฌํฉํ ๋งํด๋ณผ ์ ์์ต๋๋ค.
- Chapter 2-4. Custom Hooks์์ ์ค์ตํ๋ ๋ด์ฉ์ ๋ณต์ตํด๋ณด๋ฉฐ ๋ฆฌํฉํ ๋ง ํด๋ด ์๋ค.
๐ฎ. ๊ตฌํ ์ฝ๋
๐ App.js : ์ ์ฒด ํ๋ฉด ๋ ๋๋ง
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from './component/Navbar';
import Footer from './component/Footer';
import { useEffect, useState } from 'react';
import { Suspense, lazy } from 'react';
import useFetch from './util/useFetch';
import ScrollToTop from './Scroll';
const Home = lazy(() => import('./Home'));
const CreateBlog = lazy(() => import('./blogComponent/CreateBlog'));
const BlogDetails = lazy(() => import('./blogComponent/BlogDetail'));
const NotFound = lazy(() => import('./component/NotFound'));
// lazy๋ฅผ ์ฌ์ฉํด์ ๋์ import๋ฅผ ๊ตฌํํ๋ค.
function App() {
const { blogs, isPending, error } = useFetch('http://localhost:3001/blogs');
// useFetch ์ปค์คํ
ํ
์ ๋ง๋ค์ด์ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ๋ค.
return (
<BrowserRouter>
<ScrollToTop />
{ error && <div>{ error }</div> }
<div className="app">
<Navbar />
<div className="content">
<Suspense fallback={<div>loading...</div>}>
<Routes>
<Route exact path="/" element={<Home blogs={blogs} isPending={isPending} />} />
<Route path="/create" element={<CreateBlog />} />
<Route path="/blogs/:id" element={<BlogDetails />} />
<Route path="/blogs/:id" element={<NotFound />} />
</Routes>
</Suspense>
</div>
<Footer/>
</div>
</BrowserRouter>
);
}
export default App;
๐ BlogDetail.js : ๋ธ๋ก๊ทธ ๊ธ ํ๋ํ๋๋ฅผ ์๋ฏธ
import { useEffect, useState } from "react";
import{ Link, useNavigate, useParams } from "react-router-dom";
import useFetch from "../util/useFetch";
const BlogDetails = () => {
const [isLike, setIsLike] = useState(false);
const { id } = useParams();
// useParams๋ก url์ id๊ฐ, ์ฆ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์ ธ์์ ํด๋ฆญํ ๋ธ๋ก๊ทธ ๊ธ๋ก ๋ค์ด๊ฐ ์ ์๊ฒ ํ๋ค.
const navigate = useNavigate();
// useNavigate๋ฅผ ์ฌ์ฉํด์ ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ํ ์ ์๊ฒ ํ๋ค.
const { blogs, isPending, error } = useFetch(`http://localhost:3001/blogs/${id}`)
const blog = blogs;
const handleDeleteClick = () => {
console.log('delete!');
navigate('/');
// delete ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ค์ home์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ๋๋ค.
}
const handleLikeClick = () => {
setIsLike(!isLike);
let likeUpdate = blog.likes;
isLike === true ?
( likeUpdate > 0 ? likeUpdate = blog.likes - 1 : likeUpdate = blog.likes )
: likeUpdate = blog.likes + 1
// ๋นจ๊ฐ ํํธ์ผ๋๋ likes ์๋ฅผ +1 ํด์ฃผ๊ณ , ํ์ ํํธ์ผ๋๋ -1ํด์ค๋ค.
const putData = {
'id' : blog.id,
"title" : blog.title,
"body" : blog.body,
"author" : blog.author,
"likes" : likeUpdate,
}
// ๊ธฐ์กด ๋ฐ์ดํฐ์์ likes ์๋ง ๋ฐ๋ ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ฃผ๊ณ
fetch(`http://localhost:3001/blogs/${id}`,{
method:"PUT",
body : JSON.stringify(putData),
// fetch์ PUT ๋ฉ์๋๋ก ์ ๊ฐ์ฒด๋ฅผ ๋ณด๋ด์ค๋ค.
headers: {
'Content-Type': 'application/json'
},
})
.then( () => {
navigate(`/blogs/${blog.id}`);
window.location.reload();
// ์๋ก ๊ณ ์นจ์ด ์๋์ผ๋ก ๋๊ฒ ํ๋ค.
})
.catch( err => console.log(err) )
}
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>
</article>
)}
</div>
);
}
export default BlogDetails;
๐ CreateBlog.js : ์ ๊ธ ๋ฑ๋กํ๊ธฐ
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import useInput from "../util/useInput";
import Input from "../component/Input";
import Textarea from "../component/Textarea";
import useTextarea from "../util/useTextarea";
import Select from "../component/Select";
import useSelect from "../util/useSelect";
const CreateBlog = () => {
const [title, setTitle] = useInput('');
const [body, setBody] = useTextarea('');
const [author, setAuthor] = useSelect('๊น์ฝ๋ฉ');
// Input, Textarea, Select ๊ฐ์์ ์ปค์คํ
ํ
์ ์ฌ์ฉํ๋ค.
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
const putData = { title, body, author, likes: 0 };
// ์ ๊ฒ์๋ฌผ์ ๋ฃ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ฃผ๊ณ
fetch('http://localhost:3001/blogs',{
method:"POST",
body : JSON.stringify(putData),
// ๊ทธ ๋ฐ์ดํฐ๋ฅผ POST๋ก ๋ณด๋ด์ค๋ค.
headers: {
'Content-Type': 'application/json'
},
})
.then( () => {
navigate('/');
window.location.reload();
// Home์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ์ํค๊ณ , ์๋์ผ๋ก ์๋ก๊ณ ์นจ ํ๊ฒ ํ๋ค.
})
.catch( err => console.log(err) )
}
return (
<div className="create">
<h2>Add a New Blog</h2>
<form onSubmit={handleSubmit}>
// Input, Textarea, Select ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ค.
<Input
label={"์ ๋ชฉ"}
required
value={title}
content={setTitle}
// Input์์ content๋ onChange๊ฐ ์คํ๋๋ฉด ์คํ๋ ํจ์์ด๋ค.
// ์ฌ๊ธฐ๋ ์ ๋ชฉ์ ๋ฐ๊ฟ์ค์ผํ๋ฏ๋ก title ์ํ๋ฅผ ๋ฐ๊ฟ์ฃผ๋ setTitle์ ๋๊ฒจ์ค๋ค.
placeholder="์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์."
/>
<Textarea
required
label={"๋ด์ฉ"}
value={body}
content={setBody}
placeholder="๋ด์ฉ์ ์
๋ ฅํด์ฃผ์ธ์."
></Textarea>
<Select
label={"์์ฑ์"}
value={author}
content={setAuthor}
>
</Select>
<button>๋ฑ๋ก</button>
</form>
</div>
);
}
export default CreateBlog;
๐ useFetch.js : fetch์ custom hook
import { useState, useEffect } from 'react';
const useFetch = (url) => {
/* useState๋ฅผ ์ด์ฉํ์ฌ data, isPending, error๋ฅผ ์ ์ํ์ธ์. */
const [blogs, setBlogs] = useState();
const [isPending, setIsPending] = useState();
const [error, setError] = useState();
/* 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 ๋ฌธ์ ์์ฑํด์ฃผ์ธ์. */
// ๋ชจ๋ ๊ฐ์ return ํด์ฃผ๊ณ , ์๋ฅผ ๋ฐ๋๋ฐ์์๋ ํ์ํ ๊ฒ๋ง ๊ฐ๋ค ์ฐ๋ฉด ๋๋ค.
return {
blogs, setBlogs,
isPending, setIsPending,
error, setError
}
}
export default useFetch;
๐ Input.js : input์ ์ปดํฌ๋ํธํ
function Input({ label, content }) {
return (
<div className="name-input">
<label>{label}</label>
<input
onChange={content}
// content๋ onChange๊ฐ ์คํ๋ฌ์ ๋ ์คํ ๋ ํจ์์ด๋ค.
type="text"
/>
</div>
);
}
export default Input;
๐ useInput.js (useTextarea, useSelect ๋ ๋น์ทํ๋ค) : input์ custom hook
import { useState } from 'react';
function useInput(initialContent) {
const [content, setContent] = useState(initialContent);
// ๊ฐ์ ๋ฐ์์์
const onChange = (event) => {
setContent(event.target.value);
// input์ ์
๋ ฅํ ๋ ๋ง๋ค ๋ฐ๋ ๊ฐ์ผ๋ก ๋ฐ๊ฟ์ค๋ค.
}
return [content, onChange];
// ๋ฐ๋ ๊ฐ๊ณผ, ๊ทธ ๊ฐ์ ๋ฐ๊ฟ ์ ์๋ ํจ์๋ฅผ ๊ฐ์ด ๋ฐํํ๋ค.
}
export default useInput;
๐ Scroll.js : ์๋ก๊ณ ์นจ ํ ์คํฌ๋กค์ด ์๋์ผ๋ก ๋งจ ์๋ก ์ฌ๋ผ๊ฐ๋ ๊ธฐ๋ฅ
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
โฐ ์ด๋ ๊ฒ scroll ์ปดํฌ๋ํธ๋ฅผ ํ๋ ๋ง๋ค์ด์ฃผ๊ณ , <BrowserRouter> ๋ฐ๋ก ๋ฐ์ <ScrollToTop /> ์ด๋ ๊ฒ ๋ฃ์ด์ค๋ค => ๋ชจ๋ Router์์ ์ ์ฉ๋๋ค.
๐ฏ. ์์ฐ ํ๋ฉด
'CodeStates > Training' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
S4) Unit12. [Coz' Mini Hackathon] TodoList ๋ง๋ค๊ธฐ (0) | 2023.04.07 |
---|---|
S4) Unit 11. ์๊ณ ๋ฆฌ์ฆ ๋ฌธ์ ํ์ด(์์ด, ์กฐํฉ, Greedy, ๋ฉฑ์งํฉ) (0) | 2023.04.06 |
S4) Unit 3. [์ค์ต] ๋ฆฌ์กํธ ์น์ฑ ๋ฒ๋ค๋ง ํ ๋ฐฐํฌํ๊ธฐ (0) | 2023.03.21 |
S4) Unit 3. [์ค์ต] ๊ฐ๋จํ ์น์ฑ ๋ฒ๋ค๋ง ํ ๋ฐฐํฌํ๊ธฐ (0) | 2023.03.20 |
S4) Unit 2. [์ค์ต] ๋ฐ์ํ ์น ๊ตฌํํ๊ธฐ (0) | 2023.03.17 |