๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

react

React / Todo-List ๋งŒ๋“ค๊ธฐ (2)

1ํƒ„ : https://hydeveloper.tistory.com/191

 


 

์ด๋ฒˆ์—๋Š” useState, useEffect, useRef์™€ json.server๋ฅผ ํ™œ์šฉํ•ด CRUD ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ todoList๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•˜๋Š”๋ฐ

์ผ๋‹จ ํ•ด๋‹น ๊ณผ์ œ๋Š” ๊นƒํ—ˆ๋ธŒ์— ์—…๋กœ๋“œํ•ด๋†จ๋‹ค.. https://github.com/idubusomuch/Todo-app-json

 

์ตœ์†Œ ์š”๊ตฌ ์‚ฌํ•ญ

  1. Todo ์ƒ์„ฑ / ์กฐํšŒ / ์ˆ˜์ • / ์‚ญ์ œ (CRUD) ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.
  2. ํ˜„์žฌ ์‹œ๊ฐ„ ํ‘œ์‹œ, ํƒ€์ด๋จธ, ์Šคํ†ฑ์›Œ์น˜ ์ค‘ ํ•˜๋‚˜ ์ด์ƒ์˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”.
  3. ๋žœ๋ค ๋ช…์–ธ์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ์„ธ์š”.
  4. useState, useEffect, useRef๋ฅผ ๊ฐ๊ฐ ํ•œ ๋ฒˆ ์ด์ƒ ์‚ฌ์šฉํ•˜์„ธ์š”.
  5. ์ž์œ ๋กญ๊ฒŒ ์ ์šฉํ•ด๋ณด๊ณ  ์‹ถ์€ CSS๋ฅผ ์ž‘์„ฑํ•ด๋ณด์„ธ์š”.

์‹ฌํ™” ์š”๊ตฌ ์‚ฌํ•ญ

  1. json-server๋ฅผ ์‚ฌ์šฉํ•ด Todo ์ •๋ณด๋ฅผ ํŒŒ์ผ๋กœ ์ €์žฅํ•ด ๋ณด์„ธ์š”.
  2. Custom Hook์„ ๋งŒ๋“ค๊ณ  ์‚ฌ์šฉํ•ด ๋ณด์„ธ์š”.

 


 

 

App.jsx

๋”๋ณด๊ธฐ
import { useEffect, useState, useRef } from 'react';
import './App.css';

function App() {
  const [isLoading, data] = useFetch('http://localhost:3000/todo');
  const [todo, setTodo] = useState([]);

  useEffect(() => {
    if (data) setTodo(data);
  }, [isLoading]);

  return (
    <>
      <div className="top-bar">
        <Clock />
        <Advice />
      </div>
      {/* <StopWatch />
      <Timer /> */}
      <TodoInput setTodo={setTodo} />
      <TodoList todo={todo} setTodo={setTodo} />
    </>
  );
}

// ์ปค์Šคํ…€ ํ›…
const useFetch = (url) => {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((res) => {
        setData(res);
        setIsLoading(false);
      })
      .catch((err) => {
        setError(err);
        console.log('fetch error : ', err);
        setIsLoading(false);
      });
  }, [url]);
  return [isLoading, data, error];
};

// ๋žœ๋ค ๋ช…์–ธ ํ‘œ์‹œ
// https://github.com/gwongibeom/korean-advice-open-api
const Advice = () => {
  const [isLoading, data] = useFetch(
    'https://korean-advice-open-api.vercel.app/api/advice',
  );
  return (
    <div className="advice">
      {!isLoading && (
        <>
          <div>{data.message}</div>
          <div>-{data.author}-</div>
        </>
      )}
    </div>
  );
};

// ํ˜„์žฌ ์‹œ๊ฐ„ ํ‘œ์‹œ
const Clock = () => {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1000);
  }, []);

  return <div className="time-now">{time.toLocaleTimeString()}</div>;
};

// ์‹œ๊ฐ„ ํฌ๋งท ํ•จ์ˆ˜
const formatTime = (seconds) => {
  const hour = String(Math.floor(seconds / 3600)).padStart(2, '0');
  const minute = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
  const second = String(seconds % 60).padStart(2, '0');
  const timeString = `${hour}:${minute}:${second}`;
  return timeString;
};

// ์Šคํƒ‘์›Œ์น˜
const StopWatch = () => {
  const [time, setTime] = useState(0);
  const [isOn, setIsOn] = useState(false);
  const timeRef = useRef(null);

  useEffect(() => {
    if (isOn === true) {
      const timeId = setInterval(() => {
        setTime((prev) => prev + 1);
      }, 1000);
      timeRef.current = timeId;
    } else {
      clearInterval(timeRef.current);
    }
  }, [isOn]);

  return (
    <div>
      {formatTime(time)}
      <button onClick={() => setIsOn((prev) => !prev)}>
        {isOn ? '๋„๊ธฐ' : '์ผœ๊ธฐ'}
      </button>
      <button
        onClick={() => {
          setTime(0);
          setIsOn(false);
        }}>
        ๋ฆฌ์…‹
      </button>
    </div>
  );
};

// ํƒ€์ด๋จธ
const Timer = () => {
  // ์‹œ์ž‘ ์‹œ๊ฐ„
  const [startTime, setStartTime] = useState(0);
  // ํƒ€์ด๋จธ ์‹คํ–‰ ์—ฌ๋ถ€
  const [isOn, setIsOn] = useState(false);
  // ํ˜„์žฌ ์‹œ๊ฐ„
  const [time, setTime] = useState(0);
  // ํƒ€์ด๋จธ ์•„์ด๋””๋ฅผ ์ €์žฅํ•˜๋Š” useRef
  const timeRef = useRef(null);

  useEffect(() => {
    if (isOn && time > 0) {
      const timeId = setInterval(() => {
        setTime((prev) => prev - 1);
      }, 1000);
      timeRef.current = timeId;
    } else if (!isOn || time == 0) {
      clearInterval(timeRef.current);
    }
    // ํด๋ฆฐ์—… ํ•จ์ˆ˜
    return () => clearInterval(timeRef.current);
  }, [isOn, time]);
  return (
    <div>
      <div>
        {time ? formatTime(time) : formatTime(startTime)}
        <button
          onClick={() => {
            setIsOn(true);
            setTime(time ? time : startTime);
            setStartTime(0);
          }}>
          ์‹œ์ž‘
        </button>
        <button onClick={() => setIsOn(false)}>๋ฉˆ์ถค</button>
        <button
          onClick={() => {
            setTime(0);
            setIsOn(false);
          }}>
          ๋ฆฌ์…‹
        </button>
      </div>
      <input
        type="range"
        value={startTime}
        min="0"
        max="3600"
        step="30"
        onChange={(e) => setStartTime(e.target.value)}
      />
    </div>
  );
};

// CRUD ๊ธฐ๋Šฅ
// CREATE
const TodoInput = ({ setTodo }) => {
  const inputRef = useRef(null);
  const addTodo = () => {
    const newTodo = {
      // json server์—์„œ๋Š” ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์•„์ด๋””๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•ด์คŒ
      // id: Number(new Date()),
      content: inputRef.current.value,
      time: 0,
    };
    fetch('http://localhost:3000/todo', {
      method: 'POST',
      body: JSON.stringify(newTodo),
    })
      .then((res) => res.json())
      .then((res) => setTodo((prev) => [...prev, res]));
    inputRef.current.value = '';
  };
  return (
    <div className="todo-input">
      <input ref={inputRef} />
      <button onClick={addTodo}>์ถ”๊ฐ€</button>
    </div>
  );
};

// READ
const TodoList = ({ todo, setTodo }) => {
  return (
    <ul className="todo-list">
      {todo.map((el) => (
        <Todo key={el.id} todo={el} setTodo={setTodo} />
      ))}
    </ul>
  );
};

// UPDATE, DELETE
const Todo = ({ todo, setTodo }) => {
  const [isEdit, setIsEdit] = useState(false);
  const [editValue, setEditValue] = useState(todo.content);

  // ์ˆ˜์ •๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ
  const editHandler = () => {
    setIsEdit(true);
    setEditValue(todo.content);
  };

  // ์ˆ˜์ • ๊ธฐ๋Šฅ
  const updateTodo = () => {
    if (!editValue.trim()) {
      alert('๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
      return;
    }

    fetch(`http://localhost:3000/todo/${todo.id}`, {
      // PUT์€ ๋ฆฌ์†Œ์Šค์˜ ๋ชจ๋“  ๊ฒƒ์„ ์—…๋ฐ์ดํŠธ
      // PATCH๋Š” ๋ฆฌ์†Œ์Šค์˜ ์ผ๋ถ€๋ฅผ ์—…๋ฐ์ดํŠธ
      method: 'PATCH',
      // json-server์— ๋ณด๋‚ผ ๋ถ€๋ถ„
      body: JSON.stringify({ content: editValue }),
    })
      .then((res) => res.json())
      .then((res) => {
        setTodo((prev) =>
          prev.map((el) =>
            el.id === res.id ? { ...el, content: res.content } : el,
          ),
        );
        setIsEdit(false);
      });

    // ์ˆ˜์ • ๋
    setIsEdit(false);
  };

  // ์‚ญ์ œ ๊ธฐ๋Šฅ
  const deleteTodo = () => {
    fetch(`http://localhost:3000/todo/${todo.id}`, {
      method: 'DELETE',
    }).then((res) => {
      if (res.ok) {
        setTodo((prev) => prev.filter((el) => el.id !== todo.id));
      }
    });
  };

  return (
    <li>
      {isEdit ? (
        <>
          <input
            value={editValue}
            onChange={(e) => setEditValue(e.target.value)}
          />
          <div className="btn-container">
            <button onClick={updateTodo}>์ €์žฅ</button>
            <button onClick={() => setIsEdit(false)}>์ทจ์†Œ</button>
          </div>
        </>
      ) : (
        <>
          <div>{todo.content}</div>
          <div className="btn-container">
            <button onClick={editHandler}>์ˆ˜์ •</button>
            <button onClick={deleteTodo}>์‚ญ์ œ</button>
          </div>
        </>
      )}
    </li>
  );
};

export default App;

 

 

์ผ๋‹จ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.


App : ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ

 

* ๋ช…์–ธ ์ถœ๋ ฅ

useFetch (์ปค์Šคํ…€ ํ›…) : url์„ ๋„˜๊ธฐ๋ฉด fetch ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ๋กœ๋”ฉ / ๋ฐ์ดํ„ฐ / ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์ฃผ๋Š” ์ปค์Šคํ…€ ํ›…

Advice : useFetch๋ฅผ ์ด์šฉํ•ด ๋ช…์–ธ API๋ฅผ ๋ฐ›์•„์™€ ๋žœ๋ค ๋ช…์–ธ์„ ์ถœ๋ ฅํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ

 

* ์‹œ๊ฐ„ ๊ด€๋ จ

Clock : ์‹ค์‹œ๊ฐ„ ์‹œ๊ณ„๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ

formatTime ํ•จ์ˆ˜ : ์Šคํ†ฑ์›Œ์น˜์™€ ํƒ€์ด๋จธ์—์„œ ์‹œ๊ฐ„ ํ˜•ํƒœ๋ฅผ ํฌ๋งทํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“  ํ•จ์ˆ˜

StopWatch : ์Šคํƒ‘์›Œ์น˜ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ ์ปดํฌ๋„ŒํŠธ (์ด๋ฒˆ ๊ณผ์ œ์—์„œ๋Š” ํ™”๋ฉด์— ์•ˆํ‘œ์‹œํ•จ)

Timer : 0~1์‹œ๊ฐ„ ๋ฒ”์œ„๋กœ ํƒ€์ด๋จธ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ (์ด๋ฒˆ ๊ณผ์ œ์—์„œ๋Š” ํ™”๋ฉด์— ์•ˆํ‘œ์‹œํ•จ)

 

* todoList ๊ธฐ๋Šฅ

TodoInput : CREATE ๊ธฐ๋Šฅ

TodoList : READ ๊ธฐ๋Šฅ

Todo : UPDATE, DELETE ๊ธฐ๋Šฅ

 

 

 


 

 

 

1. useFetch

์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๋กœ๋”ฉ/๋ฐ์ดํ„ฐ/์—๋Ÿฌ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ปค์Šคํ…€ ํ›…

์ด๋ฒˆ์—” ๊ทธ๋ƒฅ json-server๋ฅผ ์ด์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— error ์ƒํƒœ ์„ค์ •์€ ํŒจ์Šคํ–ˆ๊ณ ,

async/await ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์•ˆ์ •์ ์ด๋ผ๋Š”๋ฐ ์•„์ง ์ž˜ ๋ชฐ๋ผ์„œ ํŒจ์Šค..

 

MDN - fetch() API ์‚ฌ์šฉ : https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch

const useFetch = (url) => {
  // ๋‚ด๋ถ€ ์ƒํƒœ ๊ด€๋ฆฌ
  const [isLoading, setIsLoading] = useState(true); // ์š”์ฒญ ์ง„ํ–‰ ์ƒํƒœ
  const [data, setData] = useState(null); // ์„ฑ๊ณต ์‹œ ๋ฐ›์€ json ๋ฐ์ดํ„ฐ
  // const [error, setError] = useState(null); // ์‹คํŒจ ์‹œ ๋ฐ›์€ error ๊ฐ์ฒด

  useEffect(() => {
    fetch(url) // HTTP GET ์š”์ฒญ (GET์ด ๊ธฐ๋ณธ๊ฐ’์ž„)
      .then((res) => res.json()) // ์‘๋‹ต์ด ์˜ค๋ฉด JSON์œผ๋กœ ํŒŒ์‹ฑ (json->javascript๋ผ๋Š” ๋œป)
      .then((res) => {
        setData(res); // ํŒŒ์‹ฑ๋œ ๊ฒฐ๊ณผ res๋ฅผ setData๋กœ ์„ค์ •
        setIsLoading(false); // isLoading์„ false๋กœ ์ „ํ™˜
      })
      .catch((err) => { // ๋„คํŠธ์›Œํฌ๋‚˜ ํŒŒ์‹ฑ ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ 
        // setError(err); // setError(err)๋กœ ์—๋Ÿฌ ์ €์žฅ
        console.log('fetch error : ', err); // ๋””๋ฒ„๊ทธ
        setIsLoading(false); // isLoading์„ false๋กœ ์ „ํ™˜(๋กœ๋”ฉ ์ƒํƒœ ํ•ด์ œ)
      });
  }, [url]); // ์˜์กด์„ฑ ๋ฐฐ์—ด [url], url์ด ๋ฐ”๋€”๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ API๋ฅผ ํ˜ธ์ถœ
  return [isLoading, data;
};

 

 



2. todo List CRUD ๊ธฐ๋Šฅ

 

์ผ๋‹จ json-server๊ณผ ์—ฐ๊ฒฐํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ปค์Šคํ…€ ํ›… useFetch๋กœ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ›์•„์˜จ๋‹ค.

json-server ์„ค์น˜๋Š” https://hydeveloper.tistory.com/196 ์˜ ์Šคํฌ๋กค ์ค‘๊ฐ„ ์ƒ๋ช… ์ฃผ๊ธฐ ํ•จ์ˆ˜์˜ ํ•„์š”์„ฑ์— ์žˆ๋‹ค..

 

 

App ์ปดํฌ๋„ŒํŠธ

function App() {
 // ๋กœ๋”ฉ ์ƒํƒœ
 // useFetch(url) => [isLoading, data] ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜๋จ 
  const [isLoading, data] = useFetch('http://localhost:3000/todo');
  const [todo, setTodo] = useState([]);

// useFetch๋กœ ๋ฐ›์•„์˜จ data๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ setTodo๋กœ todo ๊ฐ’ ์„ค์ •
  useEffect(() => {
    if (data) setTodo(data);
    // isLoading์„ ์˜์กด์„ฑ ๋ฐฐ์—ด๋กœ ๋„ฃ์–ด ๋กœ๋”ฉ์ด ๋๋‚œ๋‹ค๋ฉด ๋™๊ธฐํ™”์‹œํ‚ด
  }, [isLoading]);

// todoList์—๋Š” setTodo๋ฅผ props์œผ๋กœ ๋„˜๊ฒจ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
  return (
    <>
      <div className="top-bar">
        <Clock />
        <Advice />
      </div>
      <TodoInput setTodo={setTodo} />
      <TodoList todo={todo} setTodo={setTodo} />
    </>
  );
}

 

 

TodoInput ์ปดํฌ๋„ŒํŠธ - CREATE

์œ„์˜ useFetch์™€๋Š” ๋‹ฌ๋ฆฌ POST ๋ฐฉ์‹์„ ํ™œ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ,

GET์€ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•  ๋•Œ, POST๋Š” ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค.

์ด ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์ƒˆ๋กœ์šด todo ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ด ์—…๋ฐ์ดํŠธํ•˜๋ฏ€๋กœ method:'POST'๋กœ ํ–ˆ๋‹ค.

 

const TodoInput = ({ setTodo }) => {
  // useRef(null)๋กœ input ์š”์†Œ์— ์ง์ ‘ ์ ‘๊ทผ
  // inputRef.current.value๋กœ input์˜ value๊ฐ’์˜ ์ฝ๊ธฐ ๋ฐ ๋ฆฌ์…‹์ด ๊ฐ€๋Šฅ
  const inputRef = useRef(null);

  // json.server์— ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜
  const addTodo = () => {
    // ์ž…๋ ฅ๋ฐ›์€ ๊ฐ’์„ ๋‹ด์„ newTodo ๊ฐ์ฒด ์ƒ์„ฑ
    const newTodo = {
      content: inputRef.current.value,
      // time: 0, // ์ด๊ฑด ๊ฐ•์˜์—์„œ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ ๊ฑฐ๋ผ ์—†์–ด๋„ ๋จ
    };

    fetch('http://localhost:3000/todo', { 
      method: 'POST', // POST ์š”์ฒญ
      body: JSON.stringify(newTodo), // newTodo ๊ฐ์ฒด์— ๋‹ด๊ธด ๊ฐ’์„ jsonํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
    })
      .then(res => res.json()) // json์œผ๋กœ ๋‹ค์‹œ ํŒŒ์‹ฑํ•ด์„œ
      .then(res => setTodo(prev => [...prev, res])); 
      // ๊ธฐ์กด todo ๋ฐฐ์—ด ๋’ค์— ์‘๋‹ต๋ฐ›์•„ ํŒŒ์‹ฑํ•œ ๊ฐ์ฒด ์ถ”๊ฐ€
      // .catch๋กœ ์—๋Ÿฌ์ถ”๊ฐ€ ํ•ด๋„ ๋ ๋“ฏ.. 
    inputRef.current.value = ''; // input value๊ฐ’ ์ดˆ๊ธฐํ™”
  };

  return (
    <div className="todo-input">
      <input ref={inputRef} />
      <button onClick={addTodo}>์ถ”๊ฐ€</button>
    </div>
  );
};

 

 

TodoList ์ปดํฌ๋„ŒํŠธ - READ

const TodoList = ({ todo, setTodo }) => {
  // map์œผ๋กœ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง
  // key = el.id๋กœ ๊ตฌ๋ถ„
  // ๊ฐ ํ•ญ๋ชฉ๋งˆ๋‹ค Todo ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
  // Todo ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•„์š”ํ•˜๋ฏ€๋กœ ๋ฐ›์•„์˜จ props(todo, setTodo)๋ฅผ ๋‹ค์‹œ ๋ณด๋ƒ„
  return (
    <ul className="todo-list">
      {todo.map(el => (
        <Todo key={el.id} todo={el} setTodo={setTodo} />
      ))}
    </ul>
  );
};

 

 

Todo ์ปดํฌ๋„ŒํŠธ - UPDATE, DELETE

์—ฌ๊ธฐ ์ปดํฌ๋„ŒํŠธ๋Š” ๋งŽ์ด ๊ธธ์–ด์„œ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋‚˜๋ˆด๋‹ค.. 

 

1) ์ƒํƒœ ๊ด€๋ฆฌ 

const Todo = ({ todo, setTodo }) => {
  const [isEdit, setIsEdit] = useState(false); // ์ˆ˜์ • ๋ชจ๋“œ toggle
  // input์—์„œ content๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ์˜ ์ˆ˜์ •๊ฐ’ ์ƒํƒœ ๊ด€๋ฆฌ
  const [editValue, setEditValue] = useState(todo.content);

 

2) ์ˆ˜์ • ํ”„๋กœ์„ธ์Šค

METHOD๋ฅผ ๋ญ˜ ์จ์•ผํ•˜๋‚˜ ๊ณ ๋ฏผ์„ ํ–ˆ๋Š”๋ฐ

PUT์€ ๋ฆฌ์†Œ์Šค์˜ ๋ชจ๋“  ๊ฒƒ์„ ์—…๋ฐ์ดํŠธ, PATCH๋Š” ๋ฆฌ์†Œ์Šค์˜ ์ผ๋ถ€๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

๋‚˜๋Š” ๋ฆฌ์†Œ์Šค์˜ ์ผ๋ถ€(content)๋งŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ๋•Œ๋ฌธ์— PATCH๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

(PUT์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์—…๋ฐ์ดํŠธํ•  ๋•Œ todo ๊ฐ์ฒด์— ์žˆ๋Š” ๋ชจ๋“  ๊ฐ’์„ ๋‹ค์‹œ ๋„ฃ์–ด์ค˜์•ผ ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค...)

  // 1) ์ˆ˜์ • ๋ชจ๋“œ ํ•ธ๋“ค๋Ÿฌ (๋ฒ„ํŠผ ํด๋ฆญ์‹œ ํ˜ธ์ถœ)
  const editHandler = () => {
    // ์ˆ˜์ • ๋ชจ๋“œ : true => return๋ฌธ์—์„œ ํŽธ์ง‘ UI๋ฅผ ํ‘œ์‹œ
    setIsEdit(true);
    // ์ˆ˜์ • ๋ชจ๋“œ๊ฐ€ ๋ง‰ ์‹œ์ž‘๋์„ ๋•Œ์˜ editValue๋ฅผ ๊ธฐ์กด์˜ content ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”
    setEditValue(todo.content);
  };
  
  // 2) ์„œ๋ฒ„์— PATCH → ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const updateTodo = () => {
    // ์ˆ˜์ •ํ•œ ๊ฐ’์ด ๊ณต๋ฐฑ์ธ์ง€ ์ฒดํฌ
    if (!editValue.trim()) {
      alert('๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
      return;
    }

    // url = /todo/${todo.id} (์ˆ˜์ •๋œ ํ•ด๋‹น Todo ์ปดํฌ๋„ŒํŠธ์˜ id๋ฅผ ๋ฐ›์•„์˜ด)
    fetch(`/todo/${todo.id}`, {
      method: 'PATCH', // ์ˆ˜์ •์€ PATCH
      body: JSON.stringify({ content: editValue }), // ์ˆ˜์ •ํ•  ๊ฐ’์„ json์œผ๋กœ ๋ณ€ํ™˜ํ•ด ๋„ฃ์–ด์คŒ 
    })
      .then(res => res.json()) // ํŒŒ์‹ฑ ํ›„
      .then(res => { 
        setTodo(prev => // ์„œ๋ฒ„ ์‘๋‹ต res๋ฅผ setTodo๋กœ todo์— ๋ฐ˜์˜
          prev.map(el => (el.id === res.id ? {  // el.id์™€ res.id๊ฐ€ ๊ฐ™๋‹ค๋ฉด
            ...el, content: res.content } // true์ผ ๊ฒฝ์šฐ ์Šคํ”„๋ ˆ๋“œ ์—ฐ์‚ฐ์ž๋กœ ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•˜๊ณ  content๋กœ res.content๋ฅผ ๋ฐ˜์˜
            : el)) // ์•„๋‹ˆ๋ฉด ๊ทธ๋ƒฅ ๊ทธ๋Œ€๋กœ
        );
        setIsEdit(false); // ์ˆ˜์ • ๋ชจ๋“œ ๋
      });
  };

  // 3) ์‚ญ์ œ ์š”์ฒญ
  const deleteTodo = () => {
    // method : DELETE๋กœ ์‚ญ์ œ ์š”์ฒญ
    fetch(`/todo/${todo.id}`, { method: 'DELETE' })
      .then(res => {
        if (res.ok) { // ์š”์ฒญ์ด ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด
          // setTodo๋กœ Todo ๋ฐฐ์—ด ์ค‘ ์‚ญ์ œํ•œ ์•„์ด๋””์™€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ์š”์†Œ๋“ค๋กœ๋งŒ ํ•„ํ„ฐ๋ง
          setTodo(prev => prev.filter(el => el.id !== todo.id));
        }
      });
  };

 

 

  return (
    <li>
      {isEdit ? (
        // ์ˆ˜์ • ๋ชจ๋“œ์ผ ๋•Œ
        <>
          <input
            value={editValue}
            onChange={e => setEditValue(e.target.value)}
          />
          <div className="btn-container">
            <button onClick={updateTodo}>์ €์žฅ</button>
            <button onClick={() => setIsEdit(false)}>์ทจ์†Œ</button>
          </div>
        </>
      ) : (
        // ์•„๋‹ ๋•Œ
        <>
          <div>{todo.content}</div>
          <div className="btn-container">
            <button onClick={editHandler}>์ˆ˜์ •</button>
            <button onClick={deleteTodo}>์‚ญ์ œ</button>
          </div>
        </>
      )}
    </li>
  );
};

 

 

 


 

 

3. Advice ์ปดํฌ๋„ŒํŠธ

ํ•œ๊ตญ์–ด ๋ช…์–ธ api๋ฅผ ์‚ฌ์šฉํ•ด ๋ช…์–ธ์„ ํ‘œ์‹œํ–ˆ๋‹ค.

isLoading์ด false๊ฐ€ ๋˜๋ฉด data.message์™€ data.author์ด ์ถœ๋ ฅ๋œ๋‹ค. (&&์—ฐ์‚ฐ์ž๊ฐ€ ์ €๋ ‡๊ฒŒ ์“ฐ์ด๋Š”๊ฒŒ ์•„์ง ๋‚ฏ์„ค๋‹ค...) 

const Advice = () => {
  const [isLoading, data] = useFetch(
    'https://korean-advice-open-api.vercel.app/api/advice',
  );
  return (
    <div className="advice">
      {!isLoading && (
        <>
          <div>{data.message}</div>
          <div>-{data.author}-</div>
        </>
      )}
    </div>
  );
};

 

 


 

 

4. Clock ์ปดํฌ๋„ŒํŠธ

const Clock = () => {
  // ์‹œ๊ฐ„ ์ƒํƒœ ๊ด€๋ฆฌ (์ดˆ๊ธฐ๊ฐ’์€ ํ˜„์žฌ Date๋ฅผ ๊ฐ€์ ธ์˜ด)
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1000); // 1์ดˆ์— ํ•œ๋ฒˆ์”ฉ new Date๋กœ time๊ฐ’์ด ์„ค์ •๋จ
  }, []); // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ ํ•œ ๋ฒˆ ์‹คํ–‰
  // useEffect ์ž์ฒด๋Š” ๋”ฐ๋ผ์„œ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰๋˜์ง€๋งŒ, 
  // setInterval๋กœ ๋“ฑ๋ก๋œ ์ฝœ๋ฐฑ์ด ๊ณ„์† ์‹คํ–‰๋˜์–ด time์ด ๋งค์ดˆ ๊ฐฑ์‹ ๋ผ ๋ Œ๋”๋ง๋จ

  return <div className="time-now">{time.toLocaleTimeString()}</div>;
};

 

ํด๋ฆฐ์—… ํ•จ์ˆ˜๋ฅผ ์จ์ฃผ๋Š” ๊ฒƒ์ด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๊ธด ํ•˜์ง€๋งŒ, ์ด ์ƒํƒœ๋กœ๋„ ๋ฌธ์ œ์—†์ด ์ž˜ ์ž‘๋™ํ•ด์„œ ์ƒ๋žตํ–ˆ๋‹ค...

๋งŒ์•ฝ ํด๋ฆฐ์—…ํ•จ์ˆ˜๋ฅผ ์จ์ค€๋‹ค๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ 1์ดˆ๋งˆ๋‹ค ํƒ€์ด๋จธ์˜ id๋ฅผ ๋ฐ›์•„์™€ ๊ณ„์† ํด๋ฆฐ์—…ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

 

const Clock = () => {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timerId = setInterval(() => {
      setTime(new Date());
    }, 1000);

    // ํด๋ฆฐ์—… ํ•จ์ˆ˜: ์–ธ๋งˆ์šดํŠธ ์‹œ ํƒ€์ด๋จธ ํ•ด์ œ
    return () => {
      clearInterval(timerId);
    };
  }, []); // ๋งˆ์šดํŠธ ์‹œ ํ•œ ๋ฒˆ ์‹คํ–‰

  return <div className="time-now">{time.toLocaleTimeString()}</div>;
};