클론코딩도 코딩이구나 싶을정도로 강의와 다른 에러들이 발생하면 찾고 또찾고.. 혼공은 자문을 구할 사람이 어렵다는 단점이 있다.. 그나마 인강의 이점을 이용해 삽질해도 해결이 안되는것은 질문을 올려가며 구현했다. 이번에도 복습차원에서 readme파일에 정리한거에 추가내용을 적어본다.
라우팅 : 어떤 네트워크내에서 통신 데이터를 보낼 경로를 선택하는 일련의 과정(위키백과)
페이지 라우팅 : 웹서버가 요청에 명시된 경로에 따라 알맞는 페이지를 보내주는 것
MPA(Multi Page Application) : 여러개의 페이지를 준비해두었다가 요청이 들어오면 경로에 따라 적절한 페이지를 보내주는 방식
SPA(Single Page Application) : 단일 페이지어플리케이션으로 한개의 페이지뿐이다.
CSR(Client Side Rendering) : 브라우저(클라이언트)에서 알아서 페이지를 랜더링하는 방식
해당 프로젝트는 SPA방식을 따르면서 CSR로 페이지를 랜더링했다.
그리고 추가로 알아보면서 SPA, MPA, CSR, SSR의 각각장단점소개한 페이지를 발견하여 킵해둔다.
해당 프로젝트에서는 React Router Dom을 활용해 라우팅을 했다.
// :id <- id값을 useParams로 가져온다
<Route path="/diary/:id" element={<Diary />} />
// http://localhost:3000/edit?id=10&mode=dark
const [searchParams, setSearchParams] = useSearchParams();
const id = searchParams.get('id');
console.log(`id: ${id}`);
// text : 버튼에 들어갈 텍스트를 props로 받기
// type : 타입을 받아 컬러 스타일링 달리하기
// onClick : 버튼별로 각각의 함수를 실행시키기 위해 부여
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;
// 버튼 컴포넌트 특정영역 사용예시
<MyButton
type={'positive'}
text={'새 일기 쓰기'}
onClick={() => navigate('/new')}
/>
// headText : 영역별 달라지는 텍스트 props
// leftChild : 왼쪽 영역에 들어가는 컴포넌트 배치
// rightChild : 오른쪽 영역에 들어가는 컴포넌트 배치
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;
// 헤더 컴포넌트 사용예시
<MyHeader
headText={isEdit ? '일기 수정하기' : '새 일기쓰기'}
leftChild={<MyButton text={'< 뒤로가기'} onClick={() => navigate(-1)} />}
rightChild={
isEdit && (
<MyButton text={'삭제하기'} type={'negative'} onClick={handleRemove} />
)
}
/>
// 초기 랜더시 현재에 해당하는 월에 있는 일기들만 보여지기
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,
23,
56,
59,
).getTime();
setData(
diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay),
);
}
}, [curDate, diaryList]);
// 월 앞으로가기
const increaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate()),
);
};
// 월 뒤로가기
const decreaseMonth = () => {
setCurDate(
new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate()),
);
};
// 셀렉트 옵션으로 들어갈 값들을 객체로 정리, 키값과 구조를 통일해 재사용이 가능하다.
const sortOptionList = [
{ value: 'latest', name: '최신 순' },
{ value: 'oldest', name: '오래된 순' },
];
const filterOptionList = [
{ value: 'all', name: '전부 다' },
{ value: 'good', name: '좋은 감정만' },
{ value: 'bad', name: '안 좋은 감정만' },
];
// 하나의 셀렉트 컴포넌트를 만들어 두개의 필터를 제작.
const ControlMenu = ({ 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>
);
};
// 순서정렬, 감정정렬 필터 - 상태 값의 변화를 사용하기 위해 useState를 사용
const [sortType, setSortType] = useState('latest');
const [filter, setFilter] = useState('all');
// 순서 정렬 value에 따른 정렬값 반환 함수
const getProcessedDiaryList = () => {
// diaryList의 원본을 건드리지않기 위해 깊은복사
const copyList = JSON.parse(JSON.stringify(diaryList));
// 감정 정렬 콜백함수
const filterCallBack = (item) => {
if (filter === 'good') {
return parseInt(item.emotion) <= 3;
} else {
return parseInt(item.emotion) > 3;
}
};
// 날짜 정렬 콜백함수
const compare = (a, b) => {
if (sortType === 'latest') {
return parseInt(a.date) - parseInt(b.date);
} else {
return parseInt(b.date) - parseInt(a.date);
}
};
// 감정 정렬함수
const filteredList =
filter === 'all' ? copyList : copyList.filter((it) => filterCallBack(it));
// 날짜 정렬함수(감정점수를 필터시킨것을 정렬)
const sortedList = filteredList.sort(compare);
return sortedList;
};
// 초기 데이터값이 잘못넘어오거나 없을경우 에러가 나지않기위해 default값으로 빈 배열 설정해두기
DiaryList.defaultProps = {
diaryList: [],
};
const Diary = () => {
// :id 값과 일기데이터의 id 값을 매치하기 위함
const { id } = useParams();
// context를 활용해 list 가져오기
const diaryList = useContext(DiaryStateContext);
const navigate = useNavigate();
const [data, setData] = useState();
// 초기 렌더시
useEffect(() => {
// 일기데이터가 1개거나 1개 이상일 때
if (diaryList.length >= 1) {
// id 값을 사용해 가져올 일기데이터 찾기
const targetDiary = diaryList.find(
(it) => parseInt(it.id) === parseInt(id),
);
// 일기가 존재할때와 아닐때를 분기
if (targetDiary) {
setData(targetDiary);
} else {
alert('없는 일기입니다.');
// 일기가 없다면 강제로 메인페지이 이동, 뒤로가기할 수 없도록 replace 설정
navigate('/', { replace: true });
}
}
}, [id, diaryList]);
// 데이터가 없거나 불러오는 시간이 걸릴때에는 하기 텍스트 로드
if (!data) {
return <div className="DiaryPage">로딩중입니다.</div>;
} else {
// 데이터가 불러와지면 하기값 리턴하여 랜더링
const curEmotionData = emotionList.find(
(it) => parseInt(it.emotion_id) === parseInt(data.emotion),
);
return (
...
);
}
};
export default Diary;
// context로 저장해둔 함수들 불러오기
const { onCreate, onEdit, onRemove } = useContext(DiaryDispatchContext);
// 작성하기 버튼
const handleSubmi = () => {
if (content.length < 1) {
contentRef.current.focus();
return;
}
if (
window.confirm(
// isEdit = 수정하기 페이지와 새로작성하기 페이지를 나눌수 있도록 Edit페이지에서 props를 내려줌
isEdit ? '일기를 수정하시겠습니까?' : '새로운 일기를 작성하시겠습니까?',
)
) {
// 수정하기와 생성하기에 전달하는 props가 다르므로 추가 분기
if (!isEdit) {
onCreate(date, content, emotion);
} else {
onEdit(originData.id, date, content, emotion);
}
}
onCreate(date, content, emotion);
navigate('/', { replace: true });
};
이유 : 현재 컴포넌트의 흐름이 [Home → DiaryList → ControlMenu]와 같은구조로 흐르고 있는 관계로 부모컴포넌트(Home)가 리랜더링 되면서 자식요소들도 리랜더링됨
해결 : ControlMenu컴포넌트를 React.memo로 사용하여 해결
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>
);
});
이유 : [DiaryList → DiaryItem] 부모컴포넌트의 필터값 변경시 state가 변경되므로 업데이트가 발생하고 자식컴포넌트 DiaryItem도 자연스러운 재랜더링 발생
해결 : DiaryItem 컴포넌트에 React.memo 적용
export default React.memo(DiaryItem);
const handleClickEmote = useCallback((emotion) => {
setEmotion(emotion);
}, []);
이번 프로젝트에서도 data들을 localStorage에 담아 보관하는 방식으로 프로젝트를 완성시켰다. 파이어베이스를 사용해 배포 도중 에러가 발견되어 열심히 삽질중이다..