📌 한입 크기로 잘라먹는 리액트 강의를 보고 감정일기장 프로젝트를 진행했다. 이 프로젝트에서 기능을 구현할 때, 어떤 이유로 특정 기술을 선택했는지 분석해봤다.

(1) useState 가 아닌 useReducer 로 상태를 관리한 이유
(2) 외부 라이브러리가 아닌 Context를 사용한 이유
(3) 명시적 형변환
(4) 공통 컴포넌트
(5) 정렬기능으로 sort가 아닌 toSorted를 사용한 이유
(6) 클래스 네임에 템플릿 리터럴을 사용한 이유 (EmotionItem.jsx)
(7) Custom Hook을 사용한 이유
(8) 특정 페이지 이동을 위해 useParams를 사용한 이유
useState가 아닌 useReducer 로 상태를 관리한 이유감정일기장 프로젝트에서 일기장 추가, 일기장 수정, 일기장 삭제의 기능을 다루는데 useState가 아닌 useReducer 를 사용했다. useReducer 같은 경우에는 복잡한 state를 다룰 때 사용하는 Hook이다.
감정일기장에서 사용한 추가, 수정, 삭제 기능들 처럼 여러가지 기능을 적용해야 하는데 그때마다 state를 별도로 만들게 되면 관리가 힘들어지고, 가독성, 유지보수성 면에서도 떨어질 수 밖에 없다.
useReducer 를 사용하게 되면 데이터 타입만으로 구분 짓고, 액션을 명확하게 분리해서 구현하여 상태와 액션을 한 번에 관리 할 수 있다. 이처럼 useState의 단점을 보완할 수 있기 때문에 이 프로젝트에서는 useReducer를 사용한 것 같다.const reducer = (state, action) => {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((item) =>
String(item.id) === String(action.data.id) ? action.data : item
);
case "DELETE":
return state.filter((item) => String(item.id) !== String(action.data.id));
}
};
감정일기장 프로젝트에서 Context를 사용한 이유는 라이브러리 설치를 따로 해줄 필요가 없고, 전역으로 상태 관리를 쉽게 할 수 있기 때문에 사용한 것 같다.
감정일기장 프로젝트는 작은 규모이기 때문에 Recoil, Redux Toolkit과 같은 라이브러리를 사용하기에는 과할 수 있다. 그렇기 때문에 복잡한 것 없이 간단하게 상태를 관리할 수 있는 Context를 사용한 것 같다.
만약 프로젝트 규모가 커진다면 Context를 사용하지 않는 것이 좋다. 이유는 상태가 변경될 때마다 모든 하위 컴포넌트가 다시 렌더링 되는 성능 이슈가 있기 때문에 규모가 큰 프로젝트에서는 복잡한 상태와 액션을 관리할 수 있는 다른 라이브러리를 사용하는 것이 좋다.
또는 생각해볼 수 있는 것으로 외부 라이브러리 추가를 최소화 하고 싶었을 수도 있다. React의 내장기능으로만 상태 관리를 함으로써 프로젝트의 의존성을 줄이고 싶었을 수도 있다.
- Context란?
Context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있으며 부모로부터 자식에게 props를 넘겨주어야 하는 프롭스 드릴링(Props Drilling 단점을 보완할 수 있다.
- Context 는 언제 사용해야 할까?
⚠️ 사용하기 전에 주의할 점은 컴포넌트를 재사용하기 어려워지므로 꼭 필요할 때만 사용해야 한다.
props를 여러단계에 걸쳐 보내줘야 하는 값이 있을 때 특정 컴포넌트는 props의 값이 필요하지 않은 경우가 있다. 이것을 프롭스 드릴링(Props Drilling) 이라고 한다. 이때 부모에서 자식 컴포넌트로만 props를 넘겨 줄 수 있으니 불필요하게 값이 필요하지 않은 컴포넌트에게도 전달이 되는 것이다. 이런 상황일 때 전역으로 데이터 관리가 가능한 Context를 사용하면 된다.
감정일기장 프로젝트에서 로직을 구현할 때 id의 값에 명시적 형변환을 하는 로직이 많이 쓰인다. 명시적 형변환을 하는 이유의 한 가지 예시를 들어서 설명해 보자면 HTTP URL의 id는 String타입 이지만 데이터의 id는 Number 타입이다.
이때 엄격한 비교를 하게되면 타입 불일치로 인한 에러를 일으키기 때문에 불필요한 에러를 막기위해 명시적 형변환을 직접 해준 것이다. 이렇게 되면 String으로 통합이 되어 엄격한 비교를 하게 되더라도 불필요한 에러를 막을 수 있다.
const reducer = (state, action) => {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((item) =>
String(item.id) === String(action.data.id) ? action.data : item
);
case "DELETE":
return state.filter((item) => String(item.id) !== String(action.data.id));
}
};
감정일기장 프로젝트에서 공통 컴포넌트를 사용하는 이유는 똑같은 UI인데 Text만 다른 경우 Style과 Text를 일일이 적용해서 중복코드가 많아지는 것보다 props로 넘겨주면 Style은 그대로 사용하되 Text만 다르게 설정을 해줘서 일관성을 줄 수 있다. 이로 인해 코드의 재사용성이 높아진다.
유지보수성 면에서도 좋아질 수 있다. 수정이 필요한 곳에 직접 가서 수정해주지 않고, 공통 컴포넌트 한 곳에서 수정하면 공통 컴포넌트를 사용한 모든 곳에 자동으로 적용이 되니까 유지보수 하는데에 편리하다.
이외에도 불필요한 렌더링을 방지할 수 있으며 로딩시간을 줄이고, 성능을 향상시키는데에 도움이 된다.
//공통 컴포넌트 예시
import "./Button.css";
const Button = ({ text, type, onClick }) => {
return (
<button className={`Button Button_${type}`} onClick={onClick}>
{text}
</button>
);
};
export default Button;
sort가 아닌 toSorted를 사용한 이유sort는 기본 배열 메서드이다. 배열의 요소를 정렬하고 원본 배열 자체를 변경한다.toSorted 는 현재 ECMAScript의 표준 메서드는 아니지만 라이브러리에서 사용될 수 있다. 원본 배열은 변경하지 않고, 새로운 배열을 반환 한다.이 둘의 공통점은 비교함수이고 두 값을 비교해서 뺀 값이 0보다 크면 자리를 바꾸고, 0이거나 0보다 작으면 자리를 변경하지 않는다.
data는 임시 데이터이며 mockData(원본 데이터)를 복사한 것이다. data에는 날짜(createdDate)가 속성으로 들어 있다. 이 글에서 sort를 사용하지 않은 이유는 data를 직접 props로 받아서 사용하고 있으며 data는 변경되면 안 되기에 toSorted를 사용한 것 같다.
이때 들 수 있는 의문점으로 data는 원본 데이터를 복사해서 사용했는데 왜 변경되면 안 되냐에 대한 의문을 가질 수 있다. data는 원본 데이터를 복사해서 사용하는 것은 맞으나 해당 컴포넌트가 아닌 다른 컴포넌트에서도 data로 많이 사용되고 있다. 만약 정렬 기능으로 data를 수정해서 배열이 변경 되었을 때 다른 컴포넌트에서는 원인 모를 에러가 날 수도 있기 때문에 여기서는 toSorted메서드를 사용하는 것이 맞다.
다시 메서드의 차이를 정의해보자면 sort는 원본 배열을 변경하고 변경된 원본 배열을 반환한다. toSorted는 새로운 배열을 반환한다. 이 차이점으로 왜 toSorted메서드를 사용했는 지 알 수 있다.
const [sortType, setSortType] = useState("latest"); //최신순 초기값
const getSortedDate = () => {
return data.toSorted((a, b) => {
if (sortType === "oldest") {
return Number(a.createdDate) - Number(b.createdDate); //오래된 일기 먼저
} else {
return Number(b.createdDate) - Number(a.createdDate); //최신 일기 먼저
}
});
className={`Button Button_${type}`} 코드를 보면 템플릿 리터럴로 적용되어 있다. 이런식으로 백틱으로 감싸서 변수를 넣을 수 있는데 여기서 사용한 방법은 props로 넘겨받은 type을 변수로 넣어주고 Button 컴포넌트를 사용할 때마다 type에 어떤 걸 넣을 지 동적으로 className을 넣어줄 수 있다. 한 마디로 버튼마다 type을 지정해주면 Style을 다르게 줄 수 있다는 의미이다.
이것과 마찬가지로 DiaryItem도 똑같다. props로 넘겨받은 emotionId와 id의 값을 템플릿리터럴 방법으로 사용했다. 이때도 감정일기장마다 이미지를 사용할 때 emotionId를 이용해서 동적으로 바뀌게 하고, 감정일기장의 고유번호인 id는 일기장마다 고유번호가 다르기 때문에 그에 맞는 페이지 이동이 가능해진다.
type 을 지정해주면 style을 동적으로 다르게 줄 수 있다는 말은 type마다 style을 css또는 styled-components에서 어떤 효과를 줄것인 지 정해줘야 type으로 불러왔을 때 사용이 가능하다.
//Button.jsx
const Button = ({ text, type, onClick }) => {
return (
<button className={`Button Button_${type}`} onClick={onClick}>
{text}
</button>
);
};
//Button.css
.Button_POSITIVE {
color: #fff;
background-color: rgb(100, 201, 100);
}
.Button_NEGATIVE {
color: #fff;
background-color: rgb(253, 86, 95);
}
---------------
//DiaryItem.jsx
//emotionId에는 식별이 가능할 수 있도록 각각의 고유번호가 값으로 들어있음
const DiaryItem = ({ id, emotionId, createdDate, content }) => {
const nav = useNavigate();
return (
<div className="DiaryItem">
<div
className={`img_section img_section_${emotionId}`}
onClick={() => nav(`/diary/${id}`)}
>
<img src={getEmotionImage(emotionId)} />
</div>
<div className="info_section" onClick={() => nav(`/diary/${id}`)}>
<div className="created_date">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="content">{content}</div>
</div>
<div className="button_section" onClick={() => nav(`/edit/${id}`)}>
<Button text={"수정하기"} />
</div>
</div>
);
};
Custom Hook을 사용한 이유use 를 붙여야 한다return 하는 함수도 마찬가지이다.아래 코드를 다른 컴포넌트(Diary.jsx)에서 사용 되어야 하는데 기능이 똑같다고 해서 복사/붙여넣기 하는 방법으로 기능을 다시 재구현하는 방법은 절대로 좋은 방법이 아니다. 프로그래밍을 할 때 중복코드가 많아지면 그건 좋은 코드라고 할 수 없을 뿐더러 유지보수 면에서도 매우 떨어진다.
이때 useState, useEffect 등 hooks를 한번에 사용이 가능한 Custom Hook을 사용하면 이러한 단점을 보완할 수 있다. Custom Hook을 사용하게 되면 코드의 재사용성이 높아지면서 동시에 중복 코드를 막을 수 있는 장점이 있다.
import { useContext, useState, useEffect } from "react";
import { DiaryStateContext } from "../App";
import { useNavigate } from "react-router-dom";
const useDiary = (id) => {
const data = useContext(DiaryStateContext);
const [curDiaryItem, setCurDiaryItem] = useState();
const nav = useNavigate();
useEffect(() => {
const currentDiaryItem = data.find(
(item) => String(item.id) === String(id)
);
if (!currentDiaryItem) {
window.alert("존재하지 않는 일기입니다.");
nav("/", { replace: true });
}
setCurDiaryItem(currentDiaryItem);
}, [id, data]);
return curDiaryItem; // return을 해줘야 사용이 가능함
};
export default useDiary;
useParams를 사용한 이유동적 라우팅(Dynamic Routing)
useParams hook은 React Router 라이브러리에서만 사용이 가능한 Hook이다. useParams를 사용하면 해당 페이지의 세부 정보를 URL 파라미터에 따리 동적으로 이동이 가능하며 이것을 동적 라우팅이라고 한다.
<Route path="/diary/:id" element={<Diary />} />
// :id가 URL 파라미터를 의미함