- 위 사진은 최종 결과물의 홈화면이다
- 아래는 코드에 대한 분석이다.
- App.js는 React 애플리케이션의 핵심 컴포넌트 중 하나로, 전체 애플리케이션의 렌더링 구조를 정의하는 역할을 한다.
- React 애플리케이션은 컴포넌트들의 계층 구조로 이루어지며, App.js는 이 계층 구조의 최상위 컴포넌트이다.
- App.js
import React, { useEffect, useReducer, useRef } from "react";
import "./App.css";
import { BrowserRouter, Link, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import New from "./pages/New";
import Edit from "./pages/Edit";
import Diary from "./pages/Diary";
// useReducer
const reducer = (state, action) => {
let newState = [];
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
// const newItem = {
// ...action.data,
// };
// newState = [newItem, ...state];
// 위 아래 두 코드가 같은 코드이다.
newState = [action.data, ...state];
break;
}
case "REMOVE": {
newState = state.filter((it) => it.id !== action.targetId);
break;
}
case "EDIT": {
newState = state.map((it) =>
it.id === action.data.id ? { ...action.data } : it
);
break;
}
default:
return state;
}
localStorage.setItem(`diary`, JSON.stringify(newState));
return newState;
};
// Context
export const DiaryStateContext = React.createContext();
export const DiaryDispatchContext = React.createContext();
// App()
function App() {
// --------- localStorate에 저장하고 불러오는 법
// useEffect(() => {
// // localStorage.setItem("item1", 10); -> localStorage에 저장하는 방법
// // const item1 = localStorage.getItem("item1"); -> LocalStorage에 지정하는 방법
// }, []);
const [data, dispatch] = useReducer(reducer, []);
useEffect(() => {
const localData = localStorage.getItem("diary");
if (localData) {
const diaryList = JSON.parse(localData).sort(
(a, b) => parseInt(b.id) - parseInt(a.id)
);
if (diaryList.length >= 1) {
dataId.current = parseInt(diaryList[0].id) + 1;
dispatch({ type: "INIT", data: diaryList });
}
}
}, []);
const dataId = useRef(0);
//CREATE
const onCreate = (date, content, emotion) => {
dispatch({
type: "CREATE",
data: {
id: dataId.current,
date: new Date(date).getTime(),
content,
emotion,
},
});
dataId.current += 1;
};
//REMOVE
const onRemove = (targetId) => {
dispatch({ type: "REMOVE", targetId });
};
//EDIT
const onEdit = (targetId, date, content, emotion) => {
dispatch({
type: "EDIT",
data: {
id: targetId,
date: new Date(date).getTime(),
content,
emotion,
},
});
};
// return
return (
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider value={{ onCreate, onEdit, onRemove }}>
<BrowserRouter>
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/edit/:id" element={<Edit />} />
<Route path="/diary/:id" element={<Diary />} />
{/* /:id와 같이 작성하는 것이 pathVariable 방식이다. */}
</Routes>
</div>
</BrowserRouter>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
);
}
export default App;
전체 코드
- 코드 분석
- useReducer
// useReducer
const reducer = (state, action) => {
let newState = [];
switch (action.type) {
case "INIT": {
return action.data;
}
case "CREATE": {
// const newItem = {
// ...action.data,
// };
// newState = [newItem, ...state];
// 위 아래 두 코드가 같은 코드이다.
newState = [action.data, ...state];
break;
}
case "REMOVE": {
newState = state.filter((it) => it.id !== action.targetId);
break;
}
case "EDIT": {
newState = state.map((it) =>
it.id === action.data.id ? { ...action.data } : it
);
break;
}
default:
return state;
}
localStorage.setItem(`diary`, JSON.stringify(newState));
return newState;
};
- useReducer를 사용하여 상태 변화 값이 각 컴포넌트 내부에 존재하지 않아 종속적이지 않게 된다.
- useReducer를 사용하면 함수와 상태를 분리 할 수 있어 각 컴포넌트의 상태 변화에 대한 관리가 유용해지고 가독성이 좋아진다.
- 여러 컴포넌트들을 type을 통해 이름을 지정하고 reducer 함수에서 switch case문을 통해 각 컴포넌트의 상태 관리가 가능해진다.
- localStrage의 경우 javascript에서 사용하는 웹 스토리지 기술 중 하나로, 웹 스토리지를 이용하여 데이터를 저장하는 기술 중 하나이다.
- 위 프로젝트의 경우 별도의 DB를 사용하지 않기 새로 고침할 때마다 새롭게 렌더링 되며, 기존의 데이터가 사라지게 된다.
- 새로 고침 때 마다 데이터가 사라지는 것을 방지하기 위해 사용하는 기술 중 하나이다.
- Context
// Context
export const DiaryStateContext = React.createContext();
export const DiaryDispatchContext = React.createContext();
.....
- 기존의 하위 컴포넌트들은, 트리 형식으로 하위 컴포넌트들이 연결되어 있는데, 최상위 컴포넌트에서 생성한 함수를 사용하기 위해선 사용하지 않아도 중간 컴포넌트를 통해서 계속해서 전달했어야 했다.
- 하지만 Context를 사용하게 되면 최상위 컴포넌트와 바로 닿아있지 않아도 어디서든 사용할 수 있는 것이 가능해진다.
- 사용 방법은 위와 같은 createContext()를 통해 Context 객체를 생성한다.
return (
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider value={{ onCreate, onEdit, onRemove }}>
- 또 return문 안에 Provider와 value를 통해 전달하고자 하는 컴포넌트들을 내부에 작성해준다.
const Home = () => {
const diaryList = useContext(DiaryStateContext);
- 하위 컴포넌트에서 위와 같이 최상위 컴포넌트에서 전달한 Context를 사용할 수 있다.
- Router
return (
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider value={{ onCreate, onEdit, onRemove }}>
<BrowserRouter>
<div className="App">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/edit/:id" element={<Edit />} />
<Route path="/diary/:id" element={<Diary />} />
{/* /:id와 같이 작성하는 것이 pathVariable 방식이다. */}
</Routes>
</div>
</BrowserRouter>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
);
- <BrowserRouter>를 통해 router 즉, 화면을 이동 시키기 위해 Routes와 Route를 사용하기 위해 감싸준다.
- <Routes>는 <Route> 여러개를 하나에 묶기 위해 사용한다.
- <Route>에서 path는 server의 URL 뒤에 추가되는 경로를 뜻한다.
- 예를 들어 path="/new"와 같이 설정되어 있으면, 기존의 서버 주소가 localhost:3000로 설정되어 있다면 localhost:3000/new 경로로 요청이 들어오게 되면 element에 해당하는 <Home /> 컴포넌트를 불러오겠다는 의미이다.
- Home.js
import { useContext, useEffect, useState } from "react";
import { DiaryStateContext } from "../App";
import MyHeader from "./../components/MyHeader";
import MyButton from "./../components/MyButton";
import DiaryList from "../components/DiaryList";
const Home = () => {
const diaryList = useContext(DiaryStateContext);
// App.js로부터 Context를 통해 전달 받은 data를 사용하기 위한 변수이다.
const [data, setData] = useState([]);
// 페이지 Home에 보여지는 글들이 계속해서 변하기 때문에 useState를 생성해준다.
const [curDate, setCurDate] = useState(new Date());
const headText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;
// 상단 title 바꾸기
useEffect(() => {
const titleElement = document.getElementsByTagName("title")[0];
titleElement.innerHTML = `감정 일기장 - Home`;
}, []);
// 날짜 범위 지정하기
useEffect(() => {
if (diaryList.length >= 1) {
const firstDay = new Date(
curDate.getFullYear(),
curDate.getMonth(),
1
).getTime();
// console.log(new Date(firstDay));
const lastDay = new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
0,
23,
59,
59
).getTime();
// console.log(new Date(lastDay));
// 날짜 필터로 범위 지정
setData(
diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay)
);
}
}, [diaryList, curDate]);
// diaryList도 넣어줘야 한다.
useEffect(() => {
console.log(data);
}, [data]);
const increaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
);
};
const decreaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
);
};
return (
<div>
<MyHeader
headText={headText}
leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
rightChild={<MyButton text={">"} onClick={increaseMonth} />}
/>
<DiaryList diaryList={data} />
</div>
);
};
export default Home;
전체 코드
- 아래는 위 Home.js 코드를 분석할 것이다.
const Home = () => {
const diaryList = useContext(DiaryStateContext);
- useContext를 통해 최상위 컴포넌트에서 생성한 Context 중 원하는 Context와 컴포넌트를 사용할 수 있다.
const [data, setData] = useState([]);
const [curDate, setCurDate] = useState(new Date());
const headText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;
- state를 사용하여 값이 변경될 때마다 새로운 값으로 렌더링한다.
- title 바꾸기
// 상단 title 바꾸기
useEffect(() => {
const titleElement = document.getElementsByTagName("title")[0];
titleElement.innerHTML = `감정 일기장 - Home`;
}, []);
- doument를 통해 현재 웹 페이지의 DOM에 접근할 수 있는 객체이고 모든 html 문서 중 tilte요소 중 첫번째 요소를 선택하기 위한 것이다.
- 우리가 흔히 보는 이 부분을 변경하기 위한 코드이다.
- 해당 월에 속한 일기 가져오기
// 날짜 범위 지정하기
useEffect(() => {
if (diaryList.length >= 1) {
const firstDay = new Date(
curDate.getFullYear(),
curDate.getMonth(),
1
// getMonth() 다음의 1은 첫번째 날을 의미한다.
).getTime();
console.log(new Date(firstDay));
const lastDay = new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
0,
23,
59,
59
).getTime();
// getMonth() 다음의 0은 달력은 1일부터 시작하기 때문에 이전달 마지막 날을 의미한다.
console.log(new Date(lastDay));
// 날짜 필터로 범위 지정
setData(
diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay)
);
}
}, [diaryList, curDate]);
// diaryList도 넣어줘야 한다.
- 위 코드는 Context로 넘어온 diaryList 객체의 길이가 1 이상 즉, 글이 하나라도 있다면 new Date 객체를 생성해 낸다.
- firstDay의 경우 해당 달의 첫번째 날은 무조건 1일 이기 때문에 위 코드와 같이 작성하였고, 마지막 날의 경우 달마다 다르기 때문에 getMonth() + 1을 통해 현재 달의 다음 달을 선택하고 [, 0]을 사용하게 되면 전달의 마지막 날이 선택되게 된다.
- setData를 filter로 해당 현재 달에 속하는 일기의 데이터를 렌더링 하도록 한다.
- 해당 함수는 [diaryList, curDate]가 두번째 파라미터로 넘어가기 때문에 두 변수가 변경되게 되면 함수가 새롭게 렌더링 된다.
- MyHeader 컴포넌트
const MyHeader = ({ headText, leftChild, rightChild }) => {
return (
<header>
<div className="head_btn_left">{leftChild}</div>
<div className="head_text">{headText}</div>
<div className="head_btn_right">{rightChild}</div>
</header>
);
};
export default MyHeader;
- MyButton 컴포넌트
const MyButton = ({ text, type, onClick }) => {
const btnType = ["positive", "negative"].includes(type) ? type : "default";
return (
<button
className={["MyButton", `MyButton_${btnType}`].join(" ")}
onClick={onClick}
>
{text}
</button>
);
};
MyButton.defaultProps = {
type: "default",
};
export default MyButton;
- Button 컴포넌트를 생성한다. text, type, onClick 3개의 컴포넌트를 전달 받는다
1. positive, negative 둘 중에 하나의 타입을 받거나 둘 중에 하나의 타입을 받지 않으면 default에 해당한다.
- 월 변환 버튼 만들기
const increaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
);
};
const decreaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
);
};
return (
<div>
<MyHeader
headText={headText}
leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
rightChild={<MyButton text={">"} onClick={increaseMonth} />}
/>
<DiaryList diaryList={data} />
</div>
);
};
- increaseMonth와 decreaseMonth 함수를 통해 버튼 클릭 시 월이 변경되는 로직을 작성한다.
- Home 화면에는 일기의 목록이 보여야하기 때문에 DiaryList 컴포넌트를 포함 시킨다.
- DiaryList 컴포넌트
- DiaryList는 작성된 글들을 화면에 보여주는 컴포넌트다.
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import MyButton from "./MyButton";
import DiaryItem from "./DiaryItem";
const sortOptionList = [
{ value: "latest", name: "최신순" },
{ value: "oldest", name: "오래된 순" },
];
const filterOptionList = [
{ value: "all", name: "전부 다" },
{ value: "good", name: "좋은 감정만" },
{ value: "bad", name: "안좋은 감정만" },
];
const ControlMenu = React.memo(({ value, onChange, optionList }) => {
return (
<select
className="ControlMenu"
value={value}
onChange={(e) => onChange(e.target.value)}
>
{optionList.map((it, idx) => (
<option key={idx} value={it.value}>
{it.name}
</option>
))}
</select>
);
});
const DiaryList = ({ diaryList }) => {
const navigate = useNavigate();
const [sortType, setSortType] = useState("latest");
const [filter, setFilter] = useState("all");
const getProcessedDiaryList = () => {
// 감정에 따른 구분을 위한 filter 함수
const filterCallBack = (item) => {
if (filter === "good") {
return parseInt(item.emotion) <= 3;
} else {
return parseInt(item.emotion) > 3;
}
};
// 최신순 비교 compare 함수
const compare = (a, b) => {
if (sortType === "latest") {
return parseInt(b.date) - parseInt(a.date);
} else {
return parseInt(a.date) - parseInt(b.date);
}
};
const copyList = JSON.parse(JSON.stringify(diaryList));
const filteredList =
filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));
const sortedList = filteredList.sort(compare);
return sortedList;
};
return (
<div className="DiaryList">
<div className="menu_wrapper">
<div className="left_col">
<ControlMenu
value={sortType}
onChange={setSortType}
optionList={sortOptionList}
/>
<ControlMenu
value={filter}
onChange={setFilter}
optionList={filterOptionList}
/>
</div>
<div className="right_col">
<MyButton
type={"positive"}
text={"새 일기쓰기"}
onClick={() => navigate("/new")}
/>
</div>
</div>
{getProcessedDiaryList().map((it) => (
<DiaryItem key={it.id} {...it} />
))}
</div>
);
};
DiaryList.defaultProps = {
diaryList: [],
};
export default DiaryList;
- state
const DiaryList = ({ diaryList }) => {
const navigate = useNavigate();
const [sortType, setSortType] = useState("latest");
const [filter, setFilter] = useState("all");
- 페이지 이동을 위한 navigate, 일기 날짜 순서를 위한 state인 sortType, 일기를 구분하기 위한 filter state를 생성한다.
- return
return (
<div className="DiaryList">
<div className="menu_wrapper">
<div className="left_col">
<ControlMenu
value={sortType}
onChange={setSortType}
optionList={sortOptionList}
/>
<ControlMenu
value={filter}
onChange={setFilter}
optionList={filterOptionList}
/>
</div>
<div className="right_col">
<MyButton
type={"positive"}
text={"새 일기쓰기"}
onClick={() => navigate("/new")}
/>
</div>
</div>
{getProcessedDiaryList().map((it) => (
<DiaryItem key={it.id} {...it} />
))}
</div>
);
};
- 화면을 보게 되면 위와 같이 검색 조건을 선택 할 수 있는 select 버튼이 있다.
- 첫번 째 ControlMenu는 최신, 오래된 순을 나누는 menu이고, 두번째 ControlMenu는 감정에 따라 나누는 menu에 해당한다.
- 각 ControlMenu의 컴포넌트로 value, onChange, optionList 3가지를 전달한다.
- 그 다음 새로운 일기를 작성하기 위한 버튼을 하나 생성해준다.
- 각 옵션 및 ControlMenu
const sortOptionList = [
{ value: "latest", name: "최신순" },
{ value: "oldest", name: "오래된 순" },
];
const filterOptionList = [
{ value: "all", name: "전부 다" },
{ value: "good", name: "좋은 감정만" },
{ value: "bad", name: "안좋은 감정만" },
];
const ControlMenu = React.memo(({ value, onChange, optionList }) => {
return (
<select
className="ControlMenu"
value={value}
onChange={(e) => onChange(e.target.value)}
>
{optionList.map((it, idx) => (
<option key={idx} value={it.value}>
{it.name}
</option>
))}
</select>
);
});
- sort,filter OptionList는 각 menu에 들어갈 객체들에 대해 정의되어 있다.
- sort는 최신, 오래된 순을 / filter는 각 감정에 따른 구분을 지어놓았다.
- ControlMenu 컴포넌트의 경우 value, onChange, optionList 3가지를 받아, select 태그를 통해 선택 메뉴를 생성하고, 변화가 일어나면 set 함수에 의해 값이 변경된다.
- 또 optionList를 통해 받아온 객체들을 가지고 각 화면에 보여질 이름들을 지정해준다.
- getProcessedDiaryList
const getProcessedDiaryList = () => {
// 감정에 따른 구분을 위한 filter 함수
const filterCallBack = (item) => {
if (filter === "good") {
return parseInt(item.emotion) <= 3;
} else {
return parseInt(item.emotion) > 3;
}
};
// 최신순 비교 compare 함수
const compare = (a, b) => {
if (sortType === "latest") {
return parseInt(b.date) - parseInt(a.date);
} else {
return parseInt(a.date) - parseInt(b.date);
}
};
const copyList = JSON.parse(JSON.stringify(diaryList));
const filteredList =
filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));
const sortedList = filteredList.sort(compare);
return sortedList;
};
- 위 함수는 state 함수에 의해 화면이 렌더링 될 때 마다 계속해서 실행된다.
- 위 코드는 이전에 생성한 select 메뉴에 대해 해당 각 선택에 대해 어떤 데이터들을 선별할 것인지 정하게 된다.
- 최종적으로 반환하는 값은 sortedList이고, sortedList는 filteredList를 compare를 기준으로 정렬한 객체에 해당한다.
- filteredList는 filter의 값이 all일 경우에는 copyList를 반환하고, all이 아닐경우 filter함수를 통해 filterCallBack 함수를 실행 시켜 데이터를 걸러낸다.
- copyList는 컴포넌트를 통해 넘어온 diaryList를 깊은 복사를 통해 해당 변수가 같은 참조 주소를 바라보는 것이 아닌 참조 주소가 다른 같은 값으로 복사하기 위해서 사용한다.
- 깊은 복사를 하지 않으면, 해당 데이터가 변하면 원본 데이터도 변경되기 때문이다.
- compare 함수의 인터페이스로 개발자가 직접 비교 내용을 작성 할 수 있다. 위 비교 내용은 sortType 즉, 최신, 오래된 순을 지정하게 되는 함수에 해당한다.
'Portfolio, Project > Project(Programming)' 카테고리의 다른 글
Vue, Spring - 간단한 블로그 만들기 - 1 (Vue 설정) (0) | 2023.09.30 |
---|---|
React - 페이지 구현(2) 글 작성 및 수정 (1) | 2023.09.28 |
React - React Router 기본, 응용 / useParams / useSearchParams / useNavigate (0) | 2023.09.24 |
React - useReducer / Context (0) | 2023.09.21 |
React - useMemo / React.memo / useCallback (0) | 2023.09.20 |