[#5] React로 Task Manager 만들기

오닐·2022년 6월 3일
0

React : Task Manager

목록 보기
5/11

🌞1. DiaryEditor

새로운 Diary를 작성하는 페이지는 My Account의 form 형식을 빌려왔는데, Diary 수정 페이지도 이와 똑같은 구성을 가지기 때문에 DiaryEditor라는 컴포넌트로 분리했다.

1-1. State

const [date, setDate] = useState(getStringDate(new Date()));
const [category, setCategory] = useState("Study");
const [title, setTitle] = useState("");
const [content, setContent] = useState("");

Diary는 작성 날짜와 카테고리(Study or Daily), 제목, 내용으로 이루어져 있다. 변경될 때마다 업데이트를 해줘야 하므로 state로 만들고, 이 form의 input과 select에 value로 전달하였다. 그리고 각각 onChange에 변경함수(e.taget.value)를 걸어서 유저가 바꾼 내용으로 업데이트 되게끔 했다.

여기서 date는 초기값을 getStringDate라는 함수를 만들어서 YYYY-MM-DD 형식으로 날짜를 가져왔다.

최근에 수강한 강의에서는 toISOString()을 사용했었는데, 이 방식은 UTC 시간대를 기준으로 하기 때문에 연월일만 반환할 때는 유저의 위치에 따라 시간이 정확하게 표기되지 않는 이슈가 있다고 한다. 그래서 해당 강의에서는 연월일을 각각 getFullYear(), getMonth(), getDate()를 이용해서 받아오고, 0~9와 같은 한 자리 수를 MM-DD의 두 자리 수 형식에 맞추기 위해 if (month < 10) { month = '0${month}';}와 같은 조건문을 사용했다.

const getStringDate = (date) => {
  let year = date.getFullYear();
  let month = String(date.getMonth() + 1).padStart(2, "0");
  let day = String(date.getDate()).padStart(2, "0");

  return `${year}-${month}-${day}`;
};

여기서 나는 조건문 대신 노마드코더 바닐라 JS 강의에서 사용했던 padStart() 메서드를 이용하여 let month = String(date.getMonth() + 1).padStart(2, "0");과 같이 표현했다. 받아온 문자열이 설정된 길이(2)에 못 미치면 정해놓은 문자(0)를 문자열의 앞(start)에 넣어서 설정된 길이를 채우는 원리이다. 코드의 길이를 줄이고 좀 더 JS스러운(?) 코드를 쓰기 위함인데, 코드의 목적을 직관적으로 보여주기에는 전자의 방식이 더 맞지 않나 싶다.

1-2. Hook

const titleRef = useRef();
const contentRef = useRef();
const navigate = useNavigate();

const { onCreate, onEdit } = useContext(DiaryDispatchContext);

title과 content를 입력하지 않으면 해당 부분에 focus를 주기 위해 useRef를 사용하고, form을 제출하면 자동으로 Diary 페이지로 보내기 위해 useNavigate를 사용했다. 그리고 미리 만들어 둔 생성 및 수정 함수를 context API를 통해서 불러왔는데

//App.js
const diaryReducer = (state, action) => {
  let newState = [];

  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      newState = [action.data, ...state];
      break;
    }
    case "EDIT": {
      newState = state.map((item) =>
        item.id === action.data.id ? action.data : item
      );
      break;
    }
    case "REMOVE": {
      newState = state.filter((item) => item.id !== action.targetId);
      break;
    }
    default: {
      return state;
    }
  }

  return newState;
};

이렇게 생긴 Reducer의 CREATE, EDIT type과

  //Create
  const onCreate = (title, content, category) => {
    dispatch({
      type: "CREATE",
      data: {
        id: dataId.current,
        date: new Date().getTime(),
        title,
        content,
        category,
      },
    });
    dataId.current += 1;
  };

  //Edit
  const onEdit = (targetId, date, title, content, category) => {
    dispatch({
      type: "EDIT",
      data: {
        id: targetId,
        date: new Date(date).getTime(),
        title,
        content,
        category,
      },
    });
  };

이렇게 생긴 함수들을 사용하기 위한 코드이다.

useReducer는 복잡한 상태 변화 로직을 분리해서 컴포넌트를 가볍게 만들어 주는 Hook이다. diaryReducer가 관리하는 state에 Context API를 적용하면 해당 state가 만들어진 컴포넌트가 아닌 다른 곳으로 전달해서 사용할 수 있다.

이렇게 최상위 컴포넌트인 App.js에서 Reducer로 만들어진 state는 Diary, NewDiary, EditDiary 등에서 사용할 것이기 때문에 향후 많은 변화를 겪을 예정이다.

const [data, dispatch] = useReducer(diaryReducer, dummyData);

//사용할 컴포넌트에 불러오기
const diaryList = useContext(DiaryStateContext);

//props로 전달하기
<FilteredList type="diary" list={diaryList} />

그래서 useReducer를 이용하여, diaryReducer로 관리하는 state를
data라는 이름으로 저장하고, Context를 이용해서 해당 컴포넌트들에 보낸 다음, 그곳에서 import 해서 사용했다.

1-3. Diary 작성 및 수정

이렇게 미리 만들어놓은 state 및 데이터, 함수, 컴포넌트들을 이용해서 Diary 작성과 수정을 구현했다.

New Diary 페이지에 들어가면 앞서 설정해 놓은 date state의 초기값 때문에 작성일의 날짜가 자동으로 입력되어 있다. date input이기 때문에 캘린더에서 날짜를 변경할 수 있지만, 상단의 onCreate 함수를 보면 해당 함수를 실행하는 당시의 시간을 data 객체에 전달하기 때문에 Diary 작성 날짜에 반영되지는 않는다. 일기를 미루지 못하게 하려는 고도의 전략...은 아니고, onEdit과 차별화를 두려는 생각이었는데 나중에 바꾼 날짜가 반영되도록 수정할지도?

const submitHandler = () => {
    if (title.length < 1) {
      titleRef.current.focus();
      return;
    }

    if (content.length < 1) {
      contentRef.current.focus();
      return;
    }

    if (
      window.confirm(
        isEdit
          ? "Do you want to edit your diary?"
          : "Do you want to publish new diary?"
      )
    ) {
      if (isEdit) {
        onEdit(originData.id, date, title, content, category);
      } else {
        onCreate(title, content, category);
      }
    }

    navigate("/diary", { replace: true });
  };

아무튼 제목과 내용, 날짜, 카테고리를 작성 및 선택한 후 Publish 버튼을 누르면 일기를 작성하겠냐는 confirm 메시지가 뜨고, 확인을 누르면 Dairy가 추가됨과 동시에 Diary 홈으로 이동된다. Publish 버튼에 onClick으로 전달한 함수는 위와 같다.

여기서 isEdit은 현재 이 DiaryEditor 컴포넌트를 NewDiary뿐만 아니라 EditDiary에서도 사용하기 때문에 New인지 Edit인지 구분하기 위해 EditDiary 페이지에서 전달할 props이다. isEdit이 true이면 Diary를 수정하는 로직이 발생 및 실행되고, false이면(isEdit을 전달하지 않으면) 작성하는 로직이 발생 및 실행된다.

Edit일 경우, 기존의 Diary에 해당하는 data를 가져다가 수정해야 하므로 이를 originData로 명명해서 DiaryEditor에 전달했다. 이와 관련해서는 하단에 설명하겠다.

const cancelHandler = () => {
    if (window.confirm("Are you sure you want to cancel it?")) {
      navigate(-1);
    }
    return;
  };

작성 및 수정을 취소하는 함수는 둘 다 동일하게 작동하므로 간단하게 작성했다. submitHandler처럼 Diary 홈으로 보내서 뒤로가기를 못하게 할까 하다가 그냥 이전 페이지로만 보내기로.

useEffect(() => {
    if (isEdit) {
      setDate(getStringDate(new Date(parseInt(originData.date))));
      setCategory(originData.category);
      setTitle(originData.title);
      setContent(originData.content);
    }
  }, [isEdit, originData]);

마지막으로 useEffect를 사용해서 DiaryEditor가 리렌더링될 때마다 위와 같은 함수가 작동하도록 만들어 주었다. Diary를 새로 작성할 때는 DiaryEditor만 렌더링되어도 상관없으나, Edit할 때는 미리 originData를 받아와야 하기 때문에 의존성 배열에 이를 판단할 수 있는 isEdit과 originData를 넣어주었다.


🌞2. EditDairy

위에서 만든 DiaryEditor만 return 시켜주어도 문제 없는 NewDiary와는 달리 EditDiary는 추가로 설정해줄 것이 몇 가지 있다.

const EditDiary = () => {
  const [originData, setOriginData] = useState();
  const { id } = useParams();
  const diaryList = useContext(DiaryStateContext);

  const navigate = useNavigate();

  useEffect(() => {
    if (diaryList.length > 0) {
      const targetDiary = diaryList.find(
        (item) => parseInt(item.id) === parseInt(id)
      );

      //targetDiary가 있을 때만
      if (targetDiary) {
        setOriginData(targetDiary);
      } else {
        navigate("/diary", { replace: true });
      }
    }
  }, [id, diaryList]);

  return (
    originData && (
      <DiaryEditor
        boxTitle="Edit Diary"
        isEdit="true"
        originData={originData}
      />
    )
  );
};

우선 특정한 Diary data를 불러와서 DiaryEditor에 넘겨야 하므로 Diary 전체 data를 diaryList라는 이름으로 불러온다.

이때 특정 Diary를 식별할 수 있도록 Diary마다 미리 부여한 id를 사용할 것이기 때문에, URL 인자들의 key-value 페어 객체를 반환하는 useParams를 사용한다. App 컴포넌트에 미리 <Route path="/edit-diary/:id" element={<EditDiary />} />로 경로를 설정해 두었고, 이 Route에서 전달한 id라는 params를 useParams를 통해서 불러온 것이다.

그래서 useEffect를 통해서 이 id와 diaryList가 변경될 때마다 컴포넌트를 리렌더링하도록 함수를 작성했다. 애초에 EditDiary라는 페이지 자체가 수정할 Diary가 있을 때만 보여질 것이기 때문에 diaryList에 저장된 객체가 있나 확인하고, 데이터가 있으면 URL에서 받아온 id와 일치하는 id의 데이터를 찾은 다음, 이를 targetDiary에 저장한다. id값이 혹시나 숫자가 아닐 경우를 대비하여 parseInt를 통해 변환해준다.

targetDiary가 있는 경우에는 originData를 해당 데이터로 변경하고, 없는 경우에는 Diary 홈으로 이동시키고 뒤로가기를 막는다. 그리고 originData를 앞서 만들어놓은 DiaryEditor에 props로 전달하면 끝.


🌞3. 오늘의 문제

  • NewDiary 페이지에서 버튼에 폼을 제출할 수 있는 submitHandler 함수를 onClick으로 연결시켰는데도 아무런 반응이 나오지 않았다. 왜 그런가 했더니 버튼을 컴포넌트로 만들 때 onClick 속성을 props로 받을 수 있게 하지 않아서였다. 컴포넌트에 이벤트 리스너를 달 때는 항상 주의해야 할 듯.

  • Edit 로직을 구현하는 과정에서 action.data is not iterable이라는 오류가 떴다. action.data가 반복될 수 없다는 뜻인데, 이때문에 해당 데이터를 전달할 수 없다는 것. 그래서 Edit 로직에 이용된 Reducer의 case "EDIT"과 이를 이용한 onEdit 함수를 살펴보았다.

     case "EDIT": {
          newState = state.map((item) =>
            item.id === action.data.id ? [...action.data] : item
          );
          break;
        }
    
    const onEdit = (targetId, date, title, content, category) => {
        dispatch({
          type: "EDIT",
          data: {
            id: targetId,
            date: new Date(date).getTime(),
            title,
            content,
            category,
          },
        });
      };

    onEdit에 인수들을 전달하면 type: "EDIT"의 dispatch를 사용해서 newState에 action.data를 전달하는 방식인데, 여기서 [...action.data]로 전달해서 문제가 발생했다. 객체인 action.data를 스프레드 연산자를 이용해서 분리해 버리니 순환할 수 없는 형태가 된 것.

     case "EDIT": {
          newState = state.map((item) =>
            item.id === action.data.id ? action.data : item
          );
          break;
        }

    이렇게 action.data를 객체 상태 그대로 전달하면 더 이상 에러가 발생하지 않는다. 웬만한 에러는 에러 메시지만 확인해도 해답을 찾을 수 있다!


🌞4. 가벼운 회고

개발 블로그를 사용하고는 있지만 정보 전달보다는 개인의 성장을 기록하는 데에 의의를 두고 있어서 블로그에 글 쓰는 게 어렵다는 생각은 해본 적이 없었는데, 이번 글을 쓰면서 느꼈다. 아, 나는 블로그를 정말 대충 운영하고 있었구나... 그러니까 안 어렵지...

어제 굉장히 코딩이 잘 돼서 룰루랄라하면서 코딩한 게 화근이었다. 중간중간 임시 글에 코딩 과정을 저장하기는 했지만, 변경사항이 워낙 많아서 나중에 코드가 확정(?)되면 정리해서 글 써야지~ 하고 정리를 미뤄놨더니 글이 너무 길어지네...?ㅎㅎ... 덕분에 DiaryEditor와 EditDiary는 5편으로 분리해야 했다... 하지만? 그래도 정리 잘 했죠? 해냈죠?

앞으로는 기능 단위로 정리하고 커밋해야겠다. 근데 뭘 많이 만들었다고 생각했는데 아직도 할 게 한 바가지군...? 열심히 하자...!

0개의 댓글