[React] 일기장 프로젝트 2. 페이지 구현 - 홈화면
이 블로그는 유데미 '한입 크기로 잘라먹는 리액트' 강의를 듣고 복습하고자 작성되었습니다.
목차
가장 먼저 홈화면의 구조를 보겠습니다.
맨 위에 현재 월을 알려주는 헤더, 그 밑에 필터가 있고, 그 아래에 일기 리스트가 있습니다.
Home 컴포넌트를 만든 후, 각 요소를 추가해봅시다.
1. 헤더
1. 날짜 state 만들고 headText 설정
import { useState } from 'react';
const [curDate, setCurDate] = useState(new Date());
const headText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;
현재 날짜 state를 만든 후, 거기서 연과 월 정보를 받아와 headText로 만들었습니다.
2. 리턴문에 틀 만들어놓기
import MyHeader from '../components/MyHeader';
import MyButton from '../components/MyButton';
return (
<div>
<MyHeader
headText={headText}
leftChild={<MyButton text={'<'} onClick={decreaseMonth} />}
rightChild={<MyButton text={'>'} onClick={increaseMonth} />}
/>
</div>
);
저번 포스트에서 만들어놓은 myHeader, myButton 컴포넌트를 import해 사용하였습니다.
3. 양옆 화살표 버튼을 눌렀을 때 호출될 increaseMonth와 decreaseMonth 함수 구현
const increaseMonth = () => {
setCurDate(
new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
curDate.getDate(),
),
);
};
const decreaseMonth = () => {
setCurDate(
new Date(
curDate.getFullYear(),
curDate.getMonth() - 1,
curDate.getDate(),
),
);
};
2. 다이어리 리스트
다음으로 다이어리 리스트를 구현해봅시다.
1. App.js에 더미데이터 만들고, data reducer의 기본값으로 주기
먼저 App 컴포넌트 밖에다 더미데이터를 만듭니다.
const dummyData = [
{
id: 1,
emotion: 1,
contents: '내용...1',
date: 1680154234554,
},
{
id: 2,
emotion: 2,
contents: '내용...2',
date: 1680154234555,
},
{
id: 3,
emotion: 3,
contents: '내용...3',
date: 1680154234556,
},
{
id: 4,
emotion: 4,
contents: '내용...4',
date: 1680154234557,
},
// 5번이 가장 최근에 쓰인 글이므로 젤 위에 뜰것
{
id: 5,
emotion: 5,
contents: '내용...5',
date: 1680154234558,
},
];
date를 각각 다르게 해서 생성하면, date 값에 따라 내림차순으로 일기 리스트가 뜰 것입니다.
이제 App 컴포넌트 안에 reducer의 기본값으로 dummyData를 줍니다.
const [data, dispatch] = useReducer(reducer, [dummyData]);
2. Home.js에서 data를 value로 했었던 DiaryStateContext 불러오기
import { useState, useContext } from 'react';
import { DiaryStateContext } from '../App';
const diaryList = useContext(DiaryStateContext);
3. 날짜에 따라 다른 월에 뜨도록 가공하기
3-1. Home 컴포넌트에서 useState 정의
const [data, setData] = useState([]);
3-2. useEffect로 diaryList나 curDate가 바뀔 때마다, 해당 월의 일기를 띄우도록 설정
diaryList는 새로 다이어리가 추가, 삭제될 때마다 화면을 업데이트시키기 위해 조건이 되었고,
curDate 역시 날짜(화면 속 월)를 이동할 때마다 화면을 업데이트하기 위해 조건으로 하였습니다.
useEffect(() => {
const firstDay = new Date(
curDate.getFullYear(),
curDate.getMonth(),
1,
).getTime();
const lastDay = new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
0,23,59,59
).getTime();
setData(
diaryList.filter((it) => {
return it.date >= firstDay && it.date <= lastDay;
}),
);
}, [diaryList, curDate]);
다 되었으면 콘솔에 찍어서 확인시켜줄 useEffect를 추가해 확인해봅시다.
useEffect(() => {
console.log(data);
}, [data]);
브라우저를 보면, 월을 바꿀 때마다 해당 월의 일기 리스트가 콘솔에 출력되는 것을 볼 수 있습니다.
4. 빈 배열일 경우 리렌더하지 않도록 만들기
화면 리렌더는 시간이 오래걸리는 작업이므로, 빈배열일 경우 할 필요가 없는 작업입니다.
빈 배열이면 리렌더가 일어나지 않도록 추가로 처리해줍시다.
useEffect(() => {
if (diaryList.length >= 1) {
const firstDay = new Date(
curDate.getFullYear(),
curDate.getMonth(),
1,
).getTime();
const lastDay = new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
0,
).getTime();
setData(
diaryList.filter((it) => {
return it.date >= firstDay && it.date <= lastDay;
}),
);
}
}, [diaryList, curDate]);
if문으로 diaryList의 길이가 1 이상일 때만 해당 코드를 실행하도록 하였습니다.
이제 다이어리 리스트를 렌더링해봅시다.
1. DiaryList 컴포넌트 새로 만들기
const DiaryList = ({ diaryList }) => {
return (
<div>
{diaryList.map((it) => {
return <div key={it.id}>{it.contents}</div>;
})}
</div>
);
};
export default DiaryList;
diaryList를 props로 받아서, 하나씩 해당 id의 내용을 보여주도록 하였습니다.
2. Home 컴포넌트에서 임포트, 리턴에 넣기
import DiaryList from '../components/DiaryList';
<DiaryList diaryList={data} />
이렇게 하면 되겠습니다.
3. 필터
다음은 필터 기능을 구현해봅시다.
화면을 보면 필터가 2개 있고, 오른쪽에 새 일기 쓰기 버튼이 있는 것을 볼 수 있습니다.
먼저 최신순, 오래된순 등 정렬 순서를 알려주는 필터를 만들어봅시다.
1. DiaryList에 정렬기능을 할 컴포넌트 만들고 DiaryList의 리턴에 컴포넌트 넣기
const ControlMenu = ({ value, onChange, optionList }) => {
return <select></select>;
};
먼저 ControlMenu라는 컴포넌트를 만들었습니다.
props의 value는 현재 select(필터)가 선택한 값,
onChange는 select가 선택한 값이 바뀌었을 때 바꾸는 기능을 수행할 함수,
optionList는 select 태그 안에 들어갈 옵션이 됩니다.
return (
<div>
<ControlMenu />
{diaryList.map((it) => {
return <div key={it.id}>{it.contents}</div>;
})}
</div>
);
리턴에 컴포넌트를 적용시켜줍니다.
2. DiaryList에 정렬방법 state를 추가, 적용
const DiaryList = ({ diaryList }) => {
const [sortType, setSortType] = useState('latest');
return (
<div>
<ControlMenu value={sortType} onChange={setSortType} />
{diaryList.map((it) => {
return <div key={it.id}>{it.contents}</div>;
})}
</div>
);
};
DiaryList.defaultProps = {
diaryList: [],
};
sortType라는 state를 추가했고, 이걸 리턴에서 ControlMenu의 value와 onChange에 적용했습니다.
diaryList prop을 받기 때문에, defaultProps도 추가하였습니다.
3. ControlMenu 리턴에 select태그 넣기, optionList를 위한 객체리스트 생성
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) => {
return (
<option key={idx} value={it.value}>
{it.name}
</option>
);
})}
</select>
);
};
sortOptionList는 컴포넌트 밖에다 정의합니다.
4. 정렬 함수 선언
[배열].sort() 해버리면 원본배열 자체가 바뀌어버려서, 깊은복사를 통해 배열을 카피해서 다루어야 합니다.
깊은 복사는 복사할 원본을 참조하는 것이 아니라 내용전체를 똑같이 가져오는 것으로,
JSON.stringify를 써서 배열을 문자열로 만든 후 복사하고, JSON.parse로 다시 배열로 바꾸어 구현할 것입니다.
이전에 설명했듯 비교함수를 직접 만들어야 하는데요.
const getProcessedDiaryList = () => {
const compare = (a, b) => {
if (sortType === 'latest') {
//내림차순 정렬해야됨 -> 뒷숫자(b)가 더 크면 1 반환
return parseInt(b.date) - parseInt(a.date);
} else {
return parseInt(a.date) - parseInt(b.date);
}
};
const copyList = JSON.parse(JSON.stringify(diaryList));
const sortedList = copyList.sort(compare);
return sortedList;
};
이렇게 compare 함수를 함수 안에 넣고, 이 함수를 사용해 정렬된 배열을 반환하도록 구현하였습니다.
5. DiaryList 리턴의 diaryList에 위의 함수 적용하기
return (
<div>
<ControlMenu
value={sortType}
onChange={setSortType}
optionList={sortOptionList}
/>
{getProcessedDiaryList().map((it) => {
return <div key={it.id}>{it.contents}</div>;
})}
</div>
);
다음으로 좋은 감정만, 나쁜 감정만, 혹은 전부다 보여줄지 결정할 필터를 만듭시다.
1. DiaryList 컴포 안에 sortType state처럼 filter state 만들기
const [filter, setFilter] = useState('all');
2. filterOptionList 만들고, DiaryList 리턴문 안에 ControlMenu 추가
const filterOptionList = [
{ value: 'all', name: '전부' },
{ value: 'good', name: '좋은 감정만' },
{ value: 'bad', name: '안좋은 감정만' },
];
<ControlMenu
value={filter}
onChange={setFilter}
optionList={filterOptionList}
/>
아까 sortOptionList때 한 것처럼 컴포넌트 밖에다 filterOptionList를 만들고,
ControlMenu를 리턴문 안에 넣습니다.
3. getProcessedDiaryList 함수 안에 필터기능 추가
const getProcessedDiaryList = () => {
const filterCallBack = (item) => {
if (filter === 'good') {
return parseInt(item.emotion) <= 3;
} else {
return parseInt(item.emotion) > 3;
}
};
const compare = (a, b) => {
if (sortType === 'latest') {
//내림차순 정렬해야됨 -> 뒷숫자(b)가 더 크면 1 반환
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;
};
copyList를 가지고 1차로 filteredList를 만들고, 그걸 정렬해 sortedList를 만들도록 했습니다.
삼항연산자를 이용해 필터 state를 보고,
all이 아닐 경우, copyList를 필터링할 함수(filterCallBack)를 적용시켜 해당하는 리스트만 띄우도록 했습니다.
필터 옆의 일기쓰기 버튼을 만들어봅시다.
useNavigate를 이용해, 버튼을 누르면 /new 페이지로 이동하도록 할 것입니다.
1. DiaryList 컴포 리턴에 MyButton 추가하기
<MyButton text={'새 일기 쓰기'} type={'positive'} />
2. useNavigate 함수 불러오기
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate()
3. 1번에서 만든 navigate 함수를 가져와 onClick에 추가하기
<MyButton
text={'새 일기 쓰기'}
type={'positive'}
onClick={() => navigate('/new')}
/>
CSS 코드는 생략하겠습니다.
CSS의 편의를 위해 DiaryList, ControlMenu 컴포넌트의 리턴 최상위div의 className을 컴포이름과 똑같이 합시다!
4. 다이어리 아이템
이제 일기 리스트에서 보여줄 일기의 미리보기 아이템을 만들어봅시다.
1. DiaryItem.js 컴포넌트 만들기, 임포트, DiaryList의 리턴에 넣기
const DiaryItem = () => {
return <div className='DiaryItem'></div>;
};
export default DiaryItem;
컴포넌트를 만들고,
{getProcessedDiaryList().map((it) => {
return (
<div key={it.id}>
{it.contents} {it.emotion}
</div>
);
})}
이렇게 DiaryList의 리턴에 id별로 넣어줍니다.
그런데 컴포넌트 하나가 너무 길어지면 가독성에 좋지 않기 때문에, spread 연산자를 사용해 코드를 줄여봅시다.
{
getProcessedDiaryList().map((it) => {
return <DiaryItem key={it.id} {...it} />;
});
}
고친 후의 리턴부분 코드입니다.
3. DiaryItem.js의 리턴 추가
const env = process.env;
env.PUBLIC_URL = env.PUBLIC_URL || '';
return (
<div className='DiaryItem'>
<div className='emotion_img_wrapper'>
<img src={process.env.PUBLIC_URL + `assets/emotion${emotion}.png`} />
</div>
<div></div>
<div></div>
</div>
);
일단은 감정 이모티콘 부분만 했습니다.
추가로 CSS작업도 해줍니다.
이제 DiaryItem의 날짜와 내용이 들어가는 가운데부분을 만듭시다.
1. 날짜 단위를 ms에서 읽을수있게 바꾸기
const strDate = new Date(parseInt(date)).toLocaleDateString();
2. 리턴태그 작업
<div className='info_wrapper'>
<div className='diary_date'>{strDate}</div>
<div className='diary_contents_preview'>{contents.slice(0, 25)}</div>
</div>
slice는 0부터 25번째 글자까지 자른다는 뜻입니다.
3. CSS.. 역시 생략하겠습니다.
일기아이템 오른쪽에 수정하기 버튼을 달면 홈화면 레이아웃은 끝이 납니다.
<div className='btn_wrapper'>
<MyButton text={'수정하기'} />
</div>
마지막으로!! useNavigate를 이용해 각 페이지로 이동하는거 구현합시다.
1. useNavigate 임포트하고 navigate 함수 만들기
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
2. 페이지 이동 함수 정의
const goDetail = () => {
navigate(`/diary/${id}`);
};
onClick={goDetail}
DiaryItem의 이모티콘이나 내용을 누르면 둘다 상세페이지로 갈 것이므로,
goDetail 함수를 만들어서 두곳에 적용해줍시다.
const goEdit = () => {
navigate(`/edit/${id}`);
};
<MyButton onClick={goEdit} text={'수정하기'} />
같은 방법으로 수정하기 버튼을 눌렀을 때 수정페이지로 이동시켰습니다.
이렇게 홈화면 구현이 끝났습니다.
다음 시간에는 일기 작성, 수정, 상세페이지를 구현해보겠습니다.