2024-05-27 리액트 숙련과제 (2)
버튼 눌러 필터링 된 data 정보를 로컬스토리지에 담고 꺼내어 활용하기
지금까지 했던 작업은 버튼을 누른 것이 재접속을 하거나 새로고침을 하면 풀려버린다. 그런데 이를 로컬 스토리지에 저장하고 이를 불러와서 렌더링한다면 사용자가 버튼을 눌러 놓은 상태를 기억하고 재접속하거나 새로고침해도 풀리지 않는다. 페이지에 처음 접속했을 때는 아무 버튼도 누르지 않았으니 전체 data를 렌더링하고, 버튼을 누를 때 로컬스토리지에 해당 월의 배열을 저장하고, 이를 불러와서 렌더링 하는 방식으로 리팩토링하고자 한다.
처음부터 이렇게 작업했다면 수월했겠지만 과제 수행 순서를 맞추다보니 대공사를 해야 하는 소요가 발생했다. 뿐만 아니라 clickedMonthBtn
상태를 App 컴포넌트에서 정의하고 props-drilling을 했었는데 리팩토링 하는 과정에서 가만 보니 App 컴포넌트에서는 라우팅만 하고 있기 때문에 Home 컴포넌트에서 자식 컴포넌트들에게 상태를 props로 전달하는 것이 가독성 측면에서도 적절한 것 같아 이것부터 옮기고 시작한다.
// App.jsx
// 라우팅만 남기고 모두 Home 컴포넌트로 옮김.
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Detail from './pages/Detail';
function App() {
return (
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/detail" element={<Detail />} />
</Routes>
);
}
export default App;
// components/Home.jsx
// 아래는 자식 컴포넌트를 import한 것이고, 가장 윗줄은 로컬스토리지를
import React, { useState, useEffect } from 'react';
import Calendar from '../Components/Calendar';
import List from '../Components/List';
import Layout from '../Components/Layout';
import FormSection from '../Components/Form';
function Home() {
// App 컴포넌트에서 정의했던 clickedMonthBtn state를 Home 컴포넌트로 옮김
// 이 state는 사용자가 누른 버튼을 상태로 정의해서 버튼, 즉 'month' 버튼의 인덱스를
// 저장하기 위해서 만든 state임.
// 명시적으로 초기값은 null로 할당함.
// 사용자가 'month' 버튼을 누르면 handle 함수에 의해서 값이 바뀌면서
// 이 state를 참조하는 컴포넌트들이 리렌더링 된다. (배경색이 바뀌어야 하는 Calendar 컴포넌트와
// 새로운 리스트로 렌더링 해야 하는 List 컴포넌트.
const [clickedMonthBtn, setClickedMonthBtn] = useState(null);
// useEffect 훅을 이용해서 로컬스토리지에 사용자가 클릭한 버튼에 대한 정보가 있는지
// 불러오는데 의존성 배열을 [] 빈 배열로 두어서 컴포넌트가 처음 렌더링 되었을 때 한 번만
// 로컬스토리지에서 데이터를 불러옴. 이렇게 하는 이유는 로컬스토리지에서 데이터를 불러오는 건
// 한 번만 하는 것이 메모리 관리 측면에서도 좋기 때문임.
useEffect(() => {
// savedMonth라는 변수를 만들고 로컬 스토리지에 selectedMonth라는 key에 할당되어 있는
// value를 할당한다.
const savedMonth = localStorage.getItem('selectedMonth');
// 그리고 savedMonth가 null이 아닌지 검사를 하는데, 페이지를 처음 로드했을 때는
// 사용자가 버튼을 누르지 않았을 테니 selectedMonth라는 key는 존재하지 않으므로,
// 값을 불러와도 null을 반환한다.
if (savedMonth !== null) {
// null이 아니라면, 즉 사용자가 어떤 버튼이라도 눌렀다면 그 값을 가져오는데
// 그 값은 "" string 타입으로 할당되어 있을 것이기에 이것을 정수로 바꿔준다.
// 두번째 매개변수 10은 10진수로 바꾸라는 의미임.
setClickedMonthBtn(parseInt(savedMonth, 10));
}
}, []);
// 다시 한 번 useEffect 훅을 사용하는데 위에서 saveMonth라는 변수에 아무 값도 없어
// null인 경우 로컬스토리지를 불러와 clickedMonthBtn의 값을 재할당 하는 코드가
// 실행되지 않으므로, 그 이후에 실행할 로직을 작성한 것.
useEffect(() => {
// 위 코드를 실행하지 않고 내려온 경우라면 clickedMonthBtn이 null인 상태일 것이기 때문에
if (clickedMonthBtn !== null) {
// 이 경우 사용자가 클릭한 버튼의 인덱스를 저장하는 state인 clickedMonthBtn을
// 로컬스토리지에 같은 이름의 key를 주어 저장함.
localStorage.setItem('selectedMonth', clickedMonthBtn);
}
// 그리고 의존성 배열에 clickedMonthBtn을 넣었는데 이것은
// 이 useEffect 훅이 컴포넌트가 처음 마운트 됐을 때와, 사용자가 누른 버튼의 값 clickedMonthBtn의
// 값이 바뀌었을 때 실행되도록 설정한 것.
// 즉 사용자가 버튼을 누르면 계속 그 누른 값이 로컬스토리지에 저장될 것임.
}, [clickedMonthBtn]);
// 월 버튼 클릭 시 호출되는 함수
// 이 함수는 MonthBtn 컴포넌트에 props로 내려서 사용할 것임.
const handleClick = (month) => {
setClickedMonthBtn(month);
};
return (
<Layout>
<FormSection />
{/* 위에서 정의한 state와 핸들 함수를 Calendar 컴포넌트에서 사용하진 않지만
Calendar 컴포넌트의 자식인 MonthBtn 컴포넌트에서 사용할 것이기에 props-drilling을
하기 위해 Calendar 컴포넌트로 props를 전달함. */}
<Calendar clickedMonthBtn={clickedMonthBtn} handleClick={handleClick} />
{/* clickedMonth 인덱스 값을 토대로 List를 필터링 하기 위해 List에도 clickedMonthBtn
state를 props로 내려줌 */}
<List clickedMonthBtn={clickedMonthBtn} />
</Layout>
);
}
export default Home;
// Components / List.jsx
import { useState, useEffect, useMemo } from 'react';
import styled from 'styled-components';
import data from '../FakeData';
const ListArea = styled.div`
padding: 20px;
background-color: #f0f4f8;
border-radius: 10px;
width: 100%;
max-width: 1200px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`;
const ListItem = styled.div`
display: flex;
justify-content: space-between;
padding: 15px;
margin: 10px 0;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
&:hover {
background-color: #e0f7fa;
}
`;
const Date = styled.span`
flex: 1;
`;
const Category = styled.span`
flex: 2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Amount = styled.span`
flex: 1;
text-align: right;
`;
const List = ({ clickedMonthBtn }) => {
// 원본배열 data를 초기상태로 filteredData 상태로 정의함.
// 사용자가 버튼을 눌러서 저장된 그 인덱스에 따라서 원본 배열을 필터링한 배열을 만들어서
// 그 월에 맞는 것들을 불러오기 위해 별도로 상태로 정의함.
const [filteredData, setFilteredData] = useState(data);
// 원본배열의 날짜 프로퍼티인 date의 값이 "2024-01-01" 형태로 되어 있어 필요한 월만
// 필터랑 하기 어려우므로, 이를 스프레드 연산자로 펼쳐서 프로퍼티 세 개, yyyy, mm, dd를 추가할 것임.
// 이렇게 하여 processedData라는 새로운 배열을 만들어 내는데,
// useMemo 훅을 사용하여 상태를 관리한다. useEffect와 차이가 있는데 이는 복잡하여 아래에서 따로 다룬다.
const processedData = useMemo(() => data.map(item => {
// 원본 배열 data를 map 메서드로 순회하며 yyyy, mm, dd라는 프로퍼티로 구조 분해 할당하고 split 메서드로
// 구분자를 구분한다.
const [yyyy, mm, dd] = item.date.split('-');
return {
...item,
yyyy: parseInt(yyyy),
mm: parseInt(mm),
dd: parseInt(dd)
};
// useMemo의 의존성 배열이 빈 배열인데, 이유는 컴포넌트가 렌더링 될 때 한 번만 이 작업을
// 실행하면 되기 때문임.
}), []);
// 이번엔 useEffect 훅을 사용하여 사용자가 재접속하더라도 누른 버튼 필터링이 풀리지 않도록 한다.
// useEffect 훅을 사용한 이유는 로컬스토리지에 저장하고 불러오는 과정을 컴포넌트가
// 리렌더링 될 때마다 할 필요가 없고 누른 버튼의 값이 바뀔 때만 실행해주면 되기 때문.
// 즉 버튼이 모여있는 Calendar 컴포넌트의 side effect인 버튼을 눌러서 필터링 한다, 또는
// 그 값을 로컬스토리지에 저장한다는 개념을 제어하기 위함.
useEffect(() => {
// 누른 버튼의 state 값이 초기 상태인 null이라면, 즉 아무것도 누르지 않았다면
// yyyy, mm, dd를 풀어서 저장한 배열 자체를 렌더링 하기 위해 렌더링에 사용되는 배열 setFilteredData를
// 기본 데이터 배열로 바꾸어 주고
if (clickedMonthBtn === null) {
setFilteredData(processedData);
} else {
// 만약 뭐라도 눌렀다면 누른 월과 일치하는 인덱스만 추출하여 새로운 배열로 반환하여 filteredData 배열의
// 값을 바꾸어 준다. +1을 한 이유는 배열의 인덱스가 1월... 시작되는데 실제 인덱스 값은 0부터 시작하기 때문임.
const filtered = processedData.filter(item => item.mm === (clickedMonthBtn + 1));
setFilteredData(filtered);
// filteredData라는 로컬스토리지 key를 만들어 월별로 추출된 데이터를 할당하고,
// selectedMonth라는 key에는 현재 누른 월의 인덱스를 할당함.
// 이후 다시 기술하겠지만 filteredData라는 배열을 로컬스토리지에 할당하는 과정은 불필요하다고 생각되어
// 다시 리펙토링 할 예정임.
localStorage.setItem('filteredData', JSON.stringify(filtered));
localStorage.setItem('selectedMonth', clickedMonthBtn);
}
// 그리고 이 useEffect 훅은 누른 버튼의 값을 저장하는 상태와, 년월일이 풀어져 있는 배열이 변경될 때마다
// 로컬스토리지의 값을 바꾸는 이 훅이 재실행 됨.
}, [clickedMonthBtn, processedData]);
// 이번에 실행하는 useEffect 훅은 값을 불러오는 훅임.
useEffect(() => {
// 로컬스토리지에서 selectedMonth의 값을 불러오는데, 페이지 초기 접속 시 아무것도 클릭하지 않은
// 상태라면 null일 것이고, null이 아니라면
const savedMonth = localStorage.getItem('selectedMonth');
if (savedMonth !== null) {
// filteredData를 불러와서 로컬스토리지에 다시 할당하는 작업임.
const savedData = localStorage.getItem('filteredData');
if (savedData) {
setFilteredData(JSON.parse(savedData));
}
}
}, [processedData]);
console.log("Current month:", clickedMonthBtn);
return (
<ListArea>
{filteredData.map(item => (
<ListItem key={item.id}>
<Date>{item.date}</Date>
<Category>{item.item} - {item.description}</Category>
<Amount>{item.amount.toLocaleString()} 원</Amount>
</ListItem>
))}
</ListArea>
);
};
export default List;
부연설명 : useEffect가 아닌 useMemo를 사용한 이유
const processedData = useMemo(() => data.map(item => {
const [yyyy, mm, dd] = item.date.split('-');
return {
...item,
yyyy: parseInt(yyyy),
mm: parseInt(mm),
dd: parseInt(dd)
};
}), []);
위 코드에서 useEffect 훅이 아닌 useMemo 훅을 사용한 이유가 있다. 두 hook은 약간의 차이가 있고 그것을 이해해야 한다. 먼저 useEffect훅과 useMeme 훅은 두 개의 매개 변수를 받고 첫번째 매개 변수는 실행할 콜백함수, 두번째 매개 변수로는 의존성 배열을 지정해준다는 점에서 사용 형태가 동일하다.
그런데 둘의 사용 목적이 다르다. useEffect는 컴포넌트가 렌더링 된 후에 발생하는 부수 효과(side effect)를 다루기 위해 사용한다. 예를 들어 버튼이 쫙 깔린 캘린더 컴포넌트가 렌더링 되고 난 후에 발생하는 부수 효과란, 버튼을 클릭해서 필터링 해야 하기 때문에 이런 과정이 side effect라고 할 수 있다. 그리고 useMemo 훅은 복잡한 계산 등에 이용되는데 컴포넌트가 리 렌더링 됨에 따라 이 계산도 불필요하게 계속 일어날 필요가 없는 경우 주로 사용한다.
두 개의 훅 모두 의존성 배열을 빈 배열로 두면 컴포넌트가 처음 렌더링 됐을 때(마운트 될 때) 한 번만 실행하고 그 이후에는 컴포넌트의 리 렌더링이 일어나더라도 실행하지 않는다. 그리고 의존성 배열에 참조할 만한 state, 변수, props 등을 넣으면 그 값이 변경될 때마다 콜백 함수가 실행된다. 그리고 useEffect 훅은 언 마운트 시(즉 컴포넌트가 화면에서 사라질 때)에 아래와 같이 clean up 함수를 이용해서 컴포넌트의 라이플 사이클에 맞춰서 실행시킬 수도 있다. useMemo는 이와 상관없기 때문에 이런 기능이 없다.
// 컴포넌트가 렌더링 됐을 때
// 'Effect 실행됨' => O
// 'Cleanup 실행됨' => X
useEffect(() => {
console.log('Effect 실행됨');
// useEffect 훅의 return문 내부의 콜백함수가 clean up 함수임
return () => {
console.log('Cleanup 실행됨');
};
}, []);
// 의존성 배열이 변경 됐을 때
// 'Cleanup 실행됨' => O
// 'Effect 실행됨' => X
// 클린업 함수 먼저 실행되는 이유는 기존의 사이드 이펙트를 모두 제거하고 새로 시작하기 위함임.
import React, { useState, useEffect } from 'react';
function ExampleComponent({ propValue }) {
useEffect(() => {
console.log('Effect 실행됨');
return () => {
console.log('Cleanup 실행됨');
};
}, [propValue]);
return <div>Check the console</div>;
}
// 컴포넌트가 언마운트 되었을 때
// Hide Component 버튼을 눌러 상태를 false로 바꿈 (언마운트 시킴)
// 'Effect 실행됨' => O
// 'Cleanup 실행됨' => X
import React, { useState, useEffect } from 'react';
function ExampleComponent() {
const [show, setShow] = useState(true);
useEffect(() => {
console.log('Effect 실행됨');
return () => {
console.log('Cleanup 실행됨');
};
}, []);
return (
<div>
{show && <div>Check the console</div>}
<button onClick={() => setShow(false)}>Hide Component</button>
</div>
);
}
useEffect에서 월별로 필터링 된 배열을 로컬스토리지에 할당하는 과정이 굳이 필요하지 않고 성능 저하를 불러올 수 있을 것 같아 리팩토링하였다. 주석 처리된 부분이 기존의 코드이고 그 아래에서 개선된 코드를 작성하였다.
useEffect(() => {
if (clickedMonthBtn === null) {
setFilteredData(processedData);
} else {
const filtered = processedData.filter(item => item.mm === (clickedMonthBtn + 1));
setFilteredData(filtered);
// 로컬 스토리지에 filteredData 를 만드는 코드를 삭제
// clickedMonthBtn 값만 로컬스토리지에 할당함.
// localStorage.setItem('filteredData', JSON.stringify(filtered));
localStorage.setItem('selectedMonth', clickedMonthBtn);
}
}, [clickedMonthBtn, processedData]);
useEffect(() => {
const savedMonth = localStorage.getItem('selectedMonth');
if (savedMonth !== null) {
// 로컬스토리지에서 filteredData를 불러오는 작업도 삭제
// const savedData = localStorage.getItem('filteredData');
// if (savedData) {
// setFilteredData(JSON.parse(savedData));
// }
// 월 별로 필터링 된 배열을 일단 로컬스토리지에 넣고 그것을 불러와서 렌더링 하는 방식이 아니라
// monthBtn이라는 클릭된 버튼의 값이 로컬스트리지에 할당되어 있으므로, 이것을 활용해서
// 원본 배열인 processedData에 직접 접근하여 렌더링을 위한 data배열을 새로 만듦.
const renderedData = processedData.filter((data) => { data.mm === savedMonth });
setFilteredData(renderedData);
}
}, [processedData]);
'Programing > TIL' 카테고리의 다른 글
2024-05-29 리덕스 개관 (1) | 2024.05.30 |
---|---|
2024-05-28 스탠다드 과제 - props-drilling을 context API 활용 리팩토링 하기 (0) | 2024.05.28 |
2024-05-24 리액트 숙련 과제 (1) (0) | 2024.05.27 |
2024-05-23 React의 상태관리 (0) | 2024.05.23 |
2024-05-22 [React] state를 이용하여 화면에 렌더링 되는 배열 필터링하기 (0) | 2024.05.21 |
댓글