Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_31일차_"리액트 useEffect, Memo프로젝트"

LEFT 2025. 1. 15. 18:38

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [31]일차

🚀31일차에는 먼저 Express 서버로 수정/삭제 기능 구현을 모두 완료하고 useEffect와 useEffect에서 cleanUp하는 부분을 배울 수 있었다.

리액트로 구현했던 Todo 리스트 프로젝트를 조금 더 개선하여 "작성날짜"와 "할일완료유무" 등을 확인할 수 있도록 Memo 프로젝트를 진행하였다.

잘 구현할 수 없을 것 같던 투두리스트 프로젝트를 기능별로 하나씩 구현해보니 시간은 꽤 오래걸렸지만 원하는 기능을 구현해낼 수 있었다.회고를 통해 프로젝트를 구현하면서 했던 생각들을 정리하고자한다!

 


리액트에 Express 적용 (할일 추가)

 

express서버의 추가하는 부분 (index.js)

let idGen = 3;
router.post("/api/todos", function (req, res) {
  let title = req.body.title;

  let todo = { id: ++idGen, title: title, done: false };
  todos.unshift(todo);
  res.status(200).json(todo);
});

 

리액트 Todo의 추가하는 부분 (TodoBox 컴포넌트)

// 1. 할 일 추가
const addTodoList = (title) => {
  const newTodo = {
    title: title,
  };

  async function addTodo(newTodo) {
    // 위에서 새로추가한 newTodo 리스트를 보내줌
    await axios.post("<http://localhost:1577/api/todos>", newTodo); // 저장을 구현하는 메소드 (express의 index.js의 post와 일치시켜줌)
    const result = await axios.get("<http://localhost:1577/api/todos>");

    setTodoList(result.data);
  }

  addTodo(newTodo);
};
//import { useRef } from "react";
import { useEffect, useState } from "react";
  • addTodoList()함수를 수정

  • const id = useRef(Math.max(...todoList.map((todo) => todo.id)) + 1);
    ➡️id값은 express에서 idGen으로 생성(=서버가 만들어주고 있음)되고 있으므로 이 값또한 필요하지 않아 주석처리
    ➡️이렇게되면 useRef를 사용하고 있지 않으므로 import문에서 useRef를 빼주어도 된다.
    ➡️express에서 let idGen 변수에 저장되어있고, 기존에 저장되어있던 1, 2, 3번 데이터를 제외한
    그 이후의 값을 받아와야하기때문에 let todo에서 id: ++idGen처럼 전위연산자를 통해
    먼저 idGen값을 증가시키고 그 id에 값을 담을 수 있도록 한다.

  • todos.unshift(todo);
    ➡️unshift()를 통해 저장한 todo값을 todos 리스트에 추가하게된다.

  • async function addTodo(newTodo)
    ➡️async 비동기로 관리되는 addTodo()함수를 추가하고
    위에서 만든 새로운 값들의 newTodo리스트를 인자로 받는다.

    ➡️저장한 데이터를 기준으로 다시 express서버로부터 새로운 데이터를 받아와서
    리액트 Todo 프로젝트의 리스트에 갱신시켜주어야하므로 async/await를 통해 함수가 비동기로 관리되도록 하는 것

  • axios.post()방식
    ➡️post()함수를 활용하여 데이터를 저장할 수 있다.
    ➡️ (express의 index.js의 post에 있는 URL과 일치시켜줌)
    ➡️첫번째 인자로는 저장할 주소를 요청한다. (localhost:1577/api/todos)
    ➡️두번째 인자로는 저장할 리스트를 보낸다. (newTodo)

  • await axios.get(URL)
    ➡️
    express서버로부터 가져온 데이터를 result에 데이터를 담는다.

  • setTodoList(result.data);
    ➡️useState의 set함수로 가져온 데이터 객체의 data부분 업데이트

  • addTodo(newTodo)
    ➡️addTodo 함수 정의 후 새로운 newTodo를 addTodo()함수의 인자로 넣어 호출한다.

“새로운 할 일을 입력” 후 브라우저를 새로고침해도 그 데이터가 사라지지 않고
백엔드 서버에 데이터가 들어있기때문에 브라우저에 그대로 유지되고 있는 것을 확인가능


리액트에 Express 적용 (할일 삭제)

 

express서버의 삭제하는 부분 (index.js)

router.delete("/api/todos/:id", function (req, res) {
  const id = parseInt(req.params.id);
  todos = todos.filter((todo) => todo.id !== id);

  res.status(200).json(todos);
});
  • express에서 확인하면 router.delete(”/api/todos/:id”)처럼 id가 URL로 들어가야하기때문에

 

리액트 Todo의 삭제하는 부분 (TodoBox 컴포넌트)

// 2. 할 일 삭제
const deleteTodoList = (id) => {
  async function deleteTodo(id){
    await axios.delete(`http://localhost:1577/api/todos/${id}`);
    const result = await axios.get("<http://localhost:1577/api/todos>");

    setTodoList(result.data);
  }

  deleteTodo(id);
};
  • await axios.delete(`http://localhost:1577/api/todos/${id}`);
    ➡️id값을 URL로 넣어주어야하기때문에 템플릿 리터럴 ` (백틱)을 활용하여 ${id}처럼 넣어준다.

  • 데이터 추가와 같이 result에 axios.get결과를 받아와 저장한 후
  • deleteTodo(id) 로 호출하여 async 함수 실행을 명시해준다.

리액트에 Express 적용 (할일 수정)

 

express서버의 수정하는 부분 (index.js)

  • index.js에 2가지의 patch가 구현되고 있는데
router.patch("/api/todos/:id", function (req, res) {
  const id = parseInt(req.params.id);
  const todo = todos.find((todo) => todo.id === id);
  todo.done = !todo.done;

  res.status(200).json(todos);
});
  • "/api/todos:/id" 를 가지는 patch는 "추후 done속성을 추가하여 "완료된 할 일"을 수정해주는 역할을 한다.

 

router.patch("/api/todos", function (req, res) {
  const id = req.body.id;
  const title = req.body.title;

  todos.map((todo) => {
    if (todo.id === id) {
      todo.title = title;
    }
  });

  res.status(200).json({ result: "ok" });
});
  • "/api/todos" 를 가지는 patch는 id, title값을 받아 수정해주고 있으므로 이 patch의 URL을 활용해야함

 

리액트 Todo의 수정하는 부분 (TodoBox 컴포넌트)

// 3. 할 일 수정
const updateTodoList = (todo) => {
  async function updateTodo(todo) {
    await axios.patch("<http://localhost:1577/api/todos>", todo);
    const result = await axios.get("<http://localhost:1577/api/todos>");

    setTodoList(result.data);
  }

  updateTodo(todo);
};
  • await axios.patch("<http://localhost:1577/api/todos>", todo);
    ➡️async함수에서 patch()를 활용하여 todo를 전달해 수정

express - localhost:1577/api /todos 접속 결과

  • express서버의 데이터를 리액트 Todo 프로젝트에 적용한 최종 결과이다.
  • id값은 이 전의 테스트(수정, 삭제)로 인해 자동 증가되어 id:6번의 데이터만 남긴 결과이다.

  • 브라우저를 새로고침하여도 추가, 삭제, 수정이 모두 잘 작동하고 있다.
    ➡️백엔드 서버를 재부팅하면 “리액트 공부하기”와 같은 데이터는 사라질 것이다.(=메모리에 저장되어있는 형태)
    🚀+추가 : 브라우저 새로고침, 백엔드 서버 재부팅에도 영향이 없는 데이터를 관리하기 위해
    데이터베이스를 활용하는 방법도 고려해볼 수 있다.

React 함수형 컴포넌트의 생명주기

  • lifecycle (라이프 사이클)이라고도 하는 "생명주기"는 프로그램이 실행되어 생성되고 소멸될때까지의 주기를 의미
  • ex.
    1. 리액트에서 함수 호출하면
    2. 해당함수가 실행
    3. 그 후 JSX로 구현된 return()부를 만나서 컴포넌트가 화면에 마운팅(=Mounting)됨

  • Mounting : 컴포넌트가 화면에 그려지는 형태 (화면에 데이터가 나타나는 것)
  • Updating : 컴포넌트가 바뀐 값으로 화면에 다시 그려지는 형태
  • UnMounting : 컴포넌트가 화면에서 삭제되는 형태 (화면에서 데이터가 사라지는 것)

  • 클래스형 컴포넌트에서 각각 다른 클래스로 Mounting, Updating, UnMounting 기능을 관리하고 있었다면
    (ex. addTodoList(), deleteTodoList(), updateTodoList()…)

  • 리액트의 함수형 컴포넌트에서는 useEffect()를 활용하여 이것들을 관리한다.
  • useEffect()는 기본적으로 Mounting될때 동작하고, Updating이나 UnMounting은 조건에 따라 동작하게된다.

▶️실습 - useEffectExam 컴포넌트 : useEffect 생명주기 실습

const UseEffectExam = () => {
  return <div>

  </div>;
};

export default UseEffectExam;

  • useEffect
    ➡️이 컴포넌트가 마운트, 업데이트, 마운트 해제될때 화면에 적용

  • useEffect( () => {} )
    ➡️렌더링 될때마다 "매번 실행"

  • useEffect( () => {}, [유효배열])
    ➡️렌더링 될때마다 "매번 실행"
    두번째 인자로 배열이 오는데 "dependency array" 라고 부름
    즉 dependency array의 값이 변경될때마다 실행

  • useEffect( () => {}, [ ])
    ➡️
    만약 두번째 인자를 빈 배열로 설정하게 되면 (= 비어있다면)
    렌더링 시 최초 한번만 실행 (=최초 한번만 실행하고자 할때 이 방법 사용)

▶️실습 - useEffect( () ⇒ { } ) 의 생명주기

const UseEffectExam2 = () => {
  console.log("UseEffectExam2 컴포넌트 렌더링!");

  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("useEffect() 실행!!");
  });

  useEffect(() => {
    console.log("useEffect() 일반 배열 [count] 실행!!");
  }, [count]);

  useEffect(() => {
    console.log("useEffect() 빈 배열 [] 실행!!");
  }, []);

  return (
    <div>
      <span>count :: {count}</span>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        UPDATE
      </button>
    </div>
  );
};

  • 버튼 클릭을 통해 useState로 상태관리가 되고 리렌더링되므로 체크해보면

  • useEffect( () => {} )
    ➡️렌더링될때마다 실행됨을 알 수 있다.

  • useEffect( () => {}, [count])
    ➡️dependency array값(=count)이 바뀔 때 계속 useEffect를 통해 계속 실행되고 있음을 알 수 있다.
    (count값을 증가시키는 setCount()함수때문에 count값이 계속 바뀌고 있음)

  • 정리하자면 빈 배열일때는 최초 한번만 실행되고 그 이후로는 렌더링 되어도 실행되지 않음

▶️실습 - 리액트 Todo의 useEffect : TodoBox 컴포넌트

useEffect(() => {
  async function getTodos() {
    const result = await axios.get("<http://localhost:1577/api/todos>");
    setTodoList(result.data);
  }

  getTodos();
}, []);
  • TodoBox컴포넌트에서 useEffect( () ⇒ { } )를 통해 express서버의 리스트를 가져오는데
    빈 배열이 두번째 인자로 들어왔으므로 "최초 실행 시 한번만 동작"하는 것 구현

useEffect()로 UnMounting기능구현

  • useEffect( () ⇒ { return () ⇒ {clean up 코드}}, [] )
    ➡️이는 언마운팅될때 이 함수가 실행됨
    return 부에 또 다른 함수가 들어오는 (함수 안의 함수) 형태

  • clean up부분에 삭제 기능 구현 가능

 

UseEffectCleanUp 컴포넌트

  • 타이머를 체크할 Timer컴포넌트 추가
const UseEffectCleanUp = () => {
	const [showTimer, setShowTimer] = useState(false);
  return <div>
    {showTimer && <Timer/>}
    <button onClick={() => setShowTimer(!showTimer)}>토글버튼</button>
  </div>;
};

export default UseEffectCleanUp;
  • const [showTimer, setShowTimer] = useState(false);
    ➡️
    showTimer를 상태관리

  • <button onClick={() => setShowTimer(!showTimer)}>토글버튼</button>
    ➡️button의 람다함수 return부로 다시 setShowTimer() 함수가 위치
    ➡️이 함수의 기능은 부정연산자로 "!showTimer"를 해줌으로써
    기본값으로 false를 가지는 showTimer값을 이벤트 발생 시 true/false로 다시 바꿔주는 역할을 수행

  • showTimer && <Timer/>
    ➡️조건부 렌더링을 구현한 것으로 showTimer가 true일때 <Timer/> 컴포넌트가 “렌더링된다”

이 패턴은 조건에 따라 특정 컴포넌트를 동적으로 렌더링하거나 제거할 때 자주 사용


Timer 컴포넌트

import { useEffect } from "react";

const Timer = () => {
  useEffect(() => {
    setInterval(() => {
      console.log("타이머 실행 중!");
    }, 1000);
  });
  return (
    <div>
      <span>타이머 시작!</span>
    </div>
  );
};

export default Timer;

  • setInterval()
    ➡️
    useEffect함수 안에 함수로 위치.
    ➡️1000ms (=1초)마다 “타이머 실행 중!”을 콘솔에 출력

  • UseEffectCleanUp 컴포넌트에서 토글버튼이 눌리면 타이머 컴포넌트가 렌더링되어 "마운트"
  • 다시 토글버튼을 눌러 타이머 컴포넌트를 언마운트해도 계속해서 “타이머 실행 중!” 콘솔 출력
    ➡️해결방법 : 타이머를 꺼줄 수 있는 기능 (종료 기능)이 cleanUp하는 코드이다.
    이는 clearInterval()이 담당

Timer컴포넌트 - clearInterval 추가

const Timer = () => {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log("타이머 실행 중!");
    }, 1000);

    return () => {
      clearInterval(timer);
      console.log("타이머 종료!");
    };
  });

  • const timer = setInterval(() => { }
    ➡️
    timer 변수에 setInterval 함수의 기능을 담아줌

  • return () => { clearInterval(timer); };
    ➡️useEffect()의 리턴부에서 clean up코드를 작성
    ➡️언마운팅될떄 이 함수 실행
    timer변수는 setInterval 함수를 담고 있으므로 이 변수를 종료시킬 수 있음

  • 결과 :
    ➡️토글버튼을 눌러 “타이머”를 활성시키면(=마운팅 Mounting) “타이머 실행 중!”이 발생
    ➡️다시 토글버튼을 눌러 “타이머” 컴포넌트를 비활성화시키면(=언마운팅 UnMounting) “타이머 종료”로 종료

리액트 함수형 컴포넌트 생명주기 정리

  1. 함수형 컴포넌트 호출
  2. 함수형 컴포넌트의 내부에서 실행
  3. return()으로 화면에 렌더링
  4. useEffect() 실행
  5. 만약 조건부가 있으면 (변경 혹은 컴포넌트의 소멸이 발생했을때 useEffect()실행)

렌더링 : Rendering, 리액트의 "컴포넌트 렌더링"은 컴포넌트 내 엘리먼트 요소들 (HTML 등)을 화면상에 그림 = 마운트
리렌더링 : re-Rendering, 리액트의 "컴포넌트 리렌더링"은 컴포넌트 내 엘리먼트 요소들 화면에 다시 그림 = 언마운트

deps의 값
(dependency array)
구조 설명
값이 없을 경우 useEffect( () => {} ) 화면이 렌더링 된 이후 수행
리렌더링 시 다시 수행
빈 배열 useEffect( () => {}, []) 화면이 렌더링 된 이후에만 수행
배열 값이 존재하는 경우 useEffect( () => {}, [값] ) 화면이 렌더링 된 이후 수행
"값이 변경되었을때" 수행

useEffect 활용

  • deps 배열이 비어있는 경우 컴포넌트가 사라질때 clean up 함수 호출

  • useEffect의 마운트 : 외부 API요청, 라이브러리 사용, setInterval 반복작업, setTimeout 작업 예약 등을 수행
  • useEffect의 언마운트 : setInterval, setTimeout을 사용하여 등록한 작업들을 clear ➡️clearInterval, clearTimeout

성능최적화를 위한 리액트 함수

useMemo

  • 컴포넌트의 성능을 최적화시킬 수 있는 대표적인 리액트의 Hook중의 하나
  • Memoization 활용
    ➡️기존 수행한 연산의 결과값을 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법

useCallback

  • 컴포넌트의 성능을 최적화시킬 수 있는 대표적인 리액트의 Hook중의 하나
  • 컴포넌트 내에서 함수를 생성하고 있다면 새로운 함수 참조값을 계속해서 만들어내고 있는 것 = 비효율적
    ➡️값이 수정될때에만 useCallback함수 사용으로 성능을 최적화

  • 의존성 부분에 빈 배열을 주면 최초에 생성된 함수를 지속적으로 기억
    ➡️메모리에 새로 할당되지 않고 동일 참조 값을 재사용

🚀실습 - 메모 프로젝트

  • NoteBox : 메모 데이터를 관리하며 전체 애플리케이션의 상태를 담당하는 컴포넌트
  • NoteInput : 새로운 메모를 추가하기 위한 입력 컴포넌트
  • NoteList : 모든 메모를 렌더링하는 컴포넌트
  • Note 컴포넌트 : 단일 메모를 렌더링하며 수정/삭제 기능을 제공하는 컴포넌트
  • +NoteCheck 컴포넌트 : 필터링된 메모를 출력할 수 있도록 필터를 교체하는 컴포넌트

예상 결과물


NoteBox 컴포넌트

const [datalist, setDatalist] = useState([
    { id: 1, title: "자료구조", createdAt: new Date(), done: true },
    { id: 2, title: "코딩인터뷰", createdAt: new Date(), done: false },
    { id: 3, title: "안드로이드", createdAt: new Date(), done: false },
  ]);
  • 샘플데이터에 createdAt 속성과 done 속성을 추가

 

const addNoteHandler = (title, date) => {
    const newNote = {
      id: id.current++,
      title: title,
      createdAt: date,
      done: false, // 새로 추가된 메모는 "미완료"적용
    };
    setDatalist([...datalist, newNote]);
  };
  • "메모"를 추가하는 부분에서는 title, date를 받아서 데이터 추가
  • 후위연산자를 활용해 useRef의 id값 증가

 

const deleteNoteHandler = (id) => {
    setDatalist(datalist.filter((note) => note.id !== id));
    // 일치하지 않는 것만 새 datalist로 만듦
};
  • filter를 이용하여 note(NoteBox컴포넌트에 담긴 기존 데이터)의 id값과 삭제할 데이터의 id값을 비교하여
    해당하지 않는 것만 새로운 배열로 저장

 

const updateNoteHandler = (afterNote) => {
    // 노트 객체를 한번에 받아와서 id를 비교하여 title과 createdAt 수정 가능
    const doneUpdate = datalist.map((beforeNote) => {
      if (beforeNote.id === afterNote.id) {
        return {
          ...beforeNote, // 기존 배열 먼저 배치
          title: afterNote.title,
          createdAt: new Date().toLocaleString(), // 수정된 현재 시점으로 작성시간 수정
        };
      }

      return beforeNote; // 수정할 id가 datalist에 존재하지 않을 경우 "기존 리스트 리턴"
    });

    setDatalist(doneUpdate); // 수정한 리스트를 현재 리스트로 적용
};
  • beforeNote (NoteBox컴포넌트에 담긴 기존 데이터), afterNote (수정된 데이터)
  • doneUpdate 변수에 ...beforeNote로 기존 배열 요소를 먼저 저장 후
    title은 새로 들어온 수정 값
    createdAt은 수정된 시점으로 갱신

NoteInput 컴포넌트

if (e.key === "Enter") {
  const uploadDate = new Date().toLocaleString();
  addNote(e.target.value, uploadDate);
  e.target.value = "";
}
  • Enter를 입력하여 "메모"가 입력된 시점의 날짜/시간 정보를 uploadDate에 담아 addNote 함수에 인자로 전달

NoteList 컴포넌트

return (
    <ul>
      {datalist.map((note) => {
        const formattedDate = note.createdAt.toLocaleString();
        return (
          <li key={note.id}>
            <Note note={note} deleteNote={deleteNote} updateNote={updateNote} />
            <h5 className="created">Created: {formattedDate}</h5>
          </li>
        );
      })}
    </ul>
);
  • const formattedDate = note.createdAt.toLocaleString();
    ➡️formattedDate 변수에 new Date()가 담긴 전달된 데이터리스트를 toLocaleString()으로 변환


Note 컴포넌트

const toggleUpdateForm = () => {
    setUpdateMode(true);
    console.log("수정 가능한 (" + updateMode + ") 상태입니다!");
  };

  const cancelUpdateForm = () => {
    setUpdateMode(false);
    console.log("수정을 취소합니다. updateMode : " + updateMode);
  };

  const changeTitleHandler = (e) => {
    setNewTitle(e.target.value);
  };

  const patchDataHandler = () => {
    updateNote({ id: note.id, title: newTitle, createdAt: note.createdAt });
    setUpdateMode(false);
};
  • const cancelUpdateForm = () => {...}
    ➡️setUpdateMode(false)를 하여 다시 수정 폼을 언마운트시키는 기능 구현

  • const patchDataHandler = () => {...}
    ➡️
    updateNote( { id, title, createdAt } )
    함수를 호출하는데 title에는 newTitle로 갱신시켜주는 것을 제외하고는 기존의 데이터리스트 속성을 그대로 보냄

 

NoteCheck 컴포넌트

  • 전체보기 / 완료된 메모 / 미완료 메모 에 대한 필터링을 수행하기 위한 컴포넌트
const NoteCheck = ({ setFilter }) => {
  const showAllNoteHandler = () => {
    setFilter("all");
  };
  const showDoneNoteHandler = () => {
    setFilter("done");
  };
  const showNotDoneNoteHandler = () => {
    setFilter("notDone");
  };

  return (
    <>
      <button onClick={showAllNoteHandler}>전체 보기</button>
      <button onClick={showDoneNoteHandler}>완료된 메모</button>
      <button onClick={showNotDoneNoteHandler}>미완료 메모</button>
    </>
  );
};

export default NoteCheck;

 

NoteBox컴포넌트에 "필터링 함수" 추가

const [filter, setFilter] = useState("all"); // 기본 값 (전체보기)

//...

const afterFiltering = () => {
    if (filter === "done") {
      // note.done자체가 boolean이므로 true인 done속성만 필터링 (완료)
      return datalist.filter((note) => note.done);
    }

    if (filter === "notDone") {
      // 부정연산자로 false인 done속성 필터링 (미완료)
      return datalist.filter((note) => !note.done);
    }

    // done, notDone 필터링에서 거쳐지지 않은 datalist
    return datalist; // 전체보기
};

return (
    <div>
      <h1 className="projectTitle">MyMeMo🐈</h1>
      <NoteInput addNote={addNoteHandler} />
      <NoteCheck setFilter={setFilter} />
      <NoteList
        datalist={afterFiltering()} // afterFiltering()의 반환값을 인자로 전달
        deleteNote={deleteNoteHandler}
        updateNote={updateNoteHandler}
      />
    </div>
  );
  • filter를 useState로 상태관리를 하고 "all"이면 전체보기, "done"이면 완료된 메모, "notDone"이면 미완료된 메모를
    마운트 할 수 있도록 적용

  • const afterFiltering = () => {...}
    ➡️
    done속성은 true, false의 boolean이므로
    filter함수에서 true이면 "done"을 반환
    filter함수에서 false이면 "notDone"을 반환
  • <NoteCheck setFilter={setFilter} />
    ➡️
    setFilter을 속성으로 전달하여 NoteCheck컴포넌트에서 필터 수행

  • <NoteList datalist={afterFiltering()} .../>
    ➡️afterFiltering()의 반환값을 인자로 전달하여 NoteList에서 필터링된 리스트를 마운트

❓filter() 함수

➡️자바스크립트의 배열 함수로 "element 배열로부터 특정 조건을 만족하는 요소만 추출해 새로운 배열로 반환"

array.filter((element) => 조건식);

 

  • 이 경우 조건식이 true인 요소만 필터링

"메모"프로젝트의 필터링을 예로 보면

if (filter === "done") {
  return datalist.filter((note) => note.done);
}
  • filter는 배열의 각 요소를 순회
  • (note) => note.done
    ➡️매개변수 note : 배열의 한 요소
    ➡️note.done : 해당 요소의 done 속성 값 반환
    ➡️note.done===true : 조건을 만족하므로 필터링

즉 note.done이 true인 요소들만 새로운 배열로 만들어 반환

메모 프로젝트 구현


🚀 회고를 통해 "메모" 프로젝트를 구현해볼 수 있었다.

useEffect의 사용으로 생명주기를 "콘솔"로 테스트해보면서 언제 실행되고 소멸되는지를 알 수 있었다.

그에 따라 마운트, 언마운트 개념에 대해 알게되었다.

이번 프로젝트에서 고민하고 생각해내는 과정이 힘들었지만 기능을 하나하나 구현해나갈 수 있었다.