[ 인프런 - 한입 크기로 잘라 먹는 React] 3. React 실전 프로젝트 - 감정 일기장 만들기(2)
인프런 이정한님의 한입크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 ] 강의 수강 후 강의의 내용을 정리하며 공부한 것을 쓴 게시글입니다.
페이지 구현 - 홈(/)
Header 세팅하기
- Home의 헤더에는 가운데 텍스트로 현재 리스트의 년/월이 있고, 왼쪽에는 이전 월로 이동하는 버튼, 오른쪽에는 이후 월로 이동하는 버튼이 있다.
- 현재 리스트의 날짜 (curDate) 는 state로 관리
const [curDate, setCurDate] = useState(new moment());
const headText = curDate.format("YYYY년 MM월");
- 왼쪽과 오른쪽 버튼을 클릭할 때마다 curDate의 월이 1씩 증가되고 감소해야 함
- moment 객체 특성 상, add 연산시 원래 값이 바뀌어버리기 때문에 clone()을 만들어 add 연산함
const increaseMonth = () => {
setCurDate(curDate.clone().add(1, "months"));
};
const decreaseMonth = () => {
setCurDate(curDate.clone().add(-1, "months"));
};
<MyHeader
headText={headText}
leftChild={<MyButton text="<" onClick={decreaseMonth} />}
rightChild={<MyButton text=">" onClick={increaseMonth} />}
/>
일기 데이터를 날짜에 맞게 불러오기
diaryList 컴포넌트
- 아직 일기 작성 로직을 만들지 않았으므로, 임시로 dummyData 생성하고 진행
- Home 컴포넌트에서 useContext로 불러와서 state로 관리함 (data)
const diaryList = useContext(DiaryStateContext);
const [data, setData] = useState([]);
- curDate가 바뀌면 해당 월마다 나타나는 일기 리스트가 다르기 때문에 각각의 일기가 몇월에 작성이 되었는지 구분되어야 함
- curDate의 clone을 만들고, moment객체의 startOf를 이용해 월의 시작일을 format함 (firatDay)
- curDate의 clone을 만들고, moment객체의 endOf를 이용해 월의 마지막일을 format함 (lastDay)
- diaryList의 date를 moment객체로 만들고. firstDay 와 lastDay 사이에 있는지 구분해 setData함
useEffect(() => {
if (diaryList.length > 0) {
const firstDay = curDate
.clone()
.startOf("month")
.format("YYYY-MM-DDThh:mm");
const lastDay = curDate.clone().endOf("month").format("YYYY-MM-DDThh:mm");
setData(
diaryList.filter((it) => moment(it.date).isBetween(firstDay, lastDay))
);
}
}, [diaryList, curDate]);
- DiaryList컴포넌트 생성
const DiaryList = ({ diaryList }) => {
return (
<div>
{diaryList.map((it) => (
<div key={it.id}>{it.content}</div>
))}
</div>
);
};
DiaryList.defaultProps = [];
export default DiaryList;
최신순 / 오래된 순으로 일기 정렬하기
- 최신순 / 오래된 순 select 추가하기
- sortType을 setState로 관리
- select의 value 값이 바뀌면 해당 값으로 setSortType 하도록 함 (lateset or oldest)
const sortOptionList = [
{ value: "latest", name: "최신순" },
{ value: "oldest", name: "오래된순" },
];
const ControlMenu = ({ value, onChange, optionList }) => {
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
{optionList.map((it, idx) => (
<option key={idx} value={it.value}>
{it.name}
</option>
))}
</select>
);
};
const [sortType, setSortType] = useState("latest");
return (
<div>
<ControlMenu
value={sortType}
onChange={setSortType}
optionList={sortOptionList}
/>
{diaryList.map((it) => (
<div key={it.id}>{it.content}</div>
))}
</div>
);
- 최신순 / 오래된 순 따라 정렬하기
- 원래 배열인 diaryList를 sort하면 원본배열이 바뀌므로 새 배열(copyList)에 깊은 복사하여 그 배열을 sort
- 깊은 복사 : JSON.parse(JSON.stringify(diaryList))
- sortType에 따라 다르게 sort하도록 compare함수 정의
- moment 객체의 diff 함수를 활용해 compare함수 정의
const getProcessedDiaryList = () => {
const compare = (a, b) => {
if (sortType === "oldest") {
return moment(a.date).diff(moment(b.date));
} else if (sortType === "latest") {
return moment(b.date).diff(moment(a.date));
}
};
const copyList = JSON.parse(JSON.stringify(diaryList));
const sortedList = copyList.sort(compare);
return sortedList;
};
- ControlMenu 컴포넌트를 불러올 떄 넘겨주는 diaryList를 getProcessedDiaryList()로 변경함
<ControlMenu
value={sortType}
onChange={setSortType}
optionList={sortOptionList}
/>
좋은 감정만 / 안좋은 감정만 일기 모아보기
- 필터를 state로 관리
- 초기값은 all
const [filter, setFilter] = useState("all");
- 모두 / 좋은 감정만 / 안좋은 감정만 select 추가하기
const filterOptionList = [
{ value: "all", name: "모두" },
{ value: "good", name: "좋은 감정만" },
{ value: "bad", name: "안좋은 감정만" },
];
<ControlMenu
value={filter}
onChange={setFilter}
optionList={filterOptionList}
/>
- 필터의 value에 따라 일기 아이템을 return할 filterCallback 함수 정의
- emtion이 3이하이면 좋은 감정이고, 3보다 크면 나쁜 감정으로 구분함
const filterCallback = (item) => {
if (filter === "good") {
return parseInt(item.emotion) <= 3;
} else if (filter === "bad") {
return parseInt(item.emotion) > 3;
}
};
- getProcessedDiaryList 함수에 감정에 따라 필터링한 filterList 정의
- 현재 filter가 all 이면 복사한 함수를 그대로 반환하고, all이 아니면 위에서 정의한 filterCallback 함수에 따라 필터링한 아이템들만 filterList에 저장
- filterList 를 최신순 / 오래된 순으로 정렬하고 리턴함
const copyList = JSON.parse(JSON.stringify(diaryList));
const filterList =
filter === "all" ? copyList : copyList.filter((it) => filterCallback(it));
const sortedList = filterList.sort(compare);
return sortedList;
새 일기쓰기 버튼 추가
- useNavigate, MyButton import
import { useNavigate } from "react-router-dom";
import MyButton from "./../components/MyButton";
- Control Menu 아래에 버튼 추가
- onClick함수에 navigate("/new") 추가해 일기 생성 페이지로 이동하도록 함
<div className="right_col">
<MyButton
type={"positive"}
text={"새 일기쓰기✏️"}
onClick={() => navigate("/new")}
/>
</div>
- 스타일링
/* DiaryList */
.DiaryList .menu_wrapper {
margin-top: 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
.DiaryList .menu_wrapper .right_col {
flex-grow: 1;
}
.DiaryList .menu_wrapper .right_col button {
width: 100%;
}
.DiaryList .ControlMenu {
margin-right: 10px;
border: none;
border-radius: 5px;
background-color: #ececec;
padding: 10px 20px 10px 20px;
cursor: pointer;
font-family: "Nanum Myeongjo", serif;
font-size: 18px;
}
일기 리스트
- DiaryItem 컴포넌트 추가
{getProcessedDiaryList().map((it) => (
<DiaryItem key={it.id} {...it} />
))}
- 감정 이미지
<div
className={[
"emotion_img_wrapper",
"emtion_img_wrapper_" + emotion,
].join(" ")}
>
<img src={process.env.PUBLIC_URL + `assets/emotion${emotion}.png`} />
</div>
- 감정 이미지 스타일링
/* DiaryItem */
.DiaryItem {
padding: 15px 0px 15px 0px;
border-bottom: 1px solid #e2e2e2;
display: flex;
justify-content: space-between;
}
.DiaryItem .emotion_img_wrapper {
cursor: pointer;
min-width: 120px;
height: 80px;
border-radius: 5px;
display: flex;
justify-content: center;
}
.DiaryItem .emotion_img_wrapper_1 {
background-color: #64c964;
}
.DiaryItem .emotion_img_wrapper_2 {
background-color: #9dd772;
}
.DiaryItem .emotion_img_wrapper_3 {
background-color: #fdce17;
}
.DiaryItem .emotion_img_wrapper_4 {
background-color: #fd8446;
}
.DiaryItem .emotion_img_wrapper_5 {
background-color: #fd565f;
}
.DiaryItem .emotion_img_wrapper img {
width: 70%;
}
- 일기 내용과 수정하기 버튼
<div onClick={() => navigete("/diary/" + id)} className="info_wrapper">
<div className="diary_date">{strDate}</div>
<div className="diary_content">{content}</div>
</div>
<div className="btn_wrapper">
<MyButton text={"수정하기"} onClick={() => navigete("/edit/" + id)} />
</div>
- 일기 내용과 수정하기 버튼 스타일링
.DiaryItem .info_wrapper {
cursor: pointer;
flex-grow: 1;
margin-left: 20px;
}
.DiaryItem .diary_date {
font-weight: bold;
font-size: 15px;
margin-bottom: 5px;
}
.DiaryItem .diary_content {
font-size: 18px;
}
.DiaryItem .btn_wrapper {
min-width: 70px;
}
.DiaryItem .btn_wrapper button {
font-size: 15px;
}