Jieunny์˜ ๋ธ”๋กœ๊ทธ

S4) Unit 4. [์‹ค์Šต] Custom Hook ๋ณธ๋ฌธ

CodeStates/Training

S4) Unit 4. [์‹ค์Šต] Custom Hook

Jieunny 2023. 3. 24. 17:29

๐Ÿ“ฃ  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์—์„œ ์ ์šฉ๋œ๋‹ค.

 

 

๐Ÿฏ. ์‹œ์—ฐ ํ™”๋ฉด