[#6] React로 Task Manager 만들기

오닐·2022년 6월 6일
0

React : Task Manager

목록 보기
6/11

⚓1. 자잘한 수정사항

  • DiaryBox에 category 배지 추가 & 랜덤 썸네일 고정
//Styled-component
const CategoryBadge = styled.span`
  display: inline-block;
  padding: 5px 10px;
  margin-bottom: 10px;

  border-radius: 20px;
  background-color: ${(props) =>
    props.category === "Study" ? "#007bff" : "#ffb400"};

  color: white;
`;

//jsx
<CategoryBadge category={category}>{category}</CategoryBadge>

정렬하지 않고는 어떤 카테고리인지 알 수가 없어서 받아온 이렇게 배지 형태로 구현해 보았다. category data를 props로 전달해서 배지의 배경색을 바꿔주었다. 추가적으로 컴포넌트가 리렌더링 될 때마다 랜덤하게 바뀌던 썸네일도 Dairy 데이터의 id번째 이미지로 고정시켰다. 이 부분은 현재 다섯 개의 이미지로 고정되어 있어서 향후에 이미지 추가 기능도 넣어볼까 고민 중이다.

  • BasicBox 컴포넌트의 자식 div에 margin-top, margin-bottom값 고정시켰던 거 삭제하고, 필요한 부분에 개별적으로 margin을 주었다. 편의성을 위해서 공통적으로 부여했던 속성이지만, div가 중첩될 때마다 필요 이상으로 margin이 들어가서 수정했다.

⚓2. Modal

작성할 내용이 많은 Diary와는 달리 WishList는 간단하게 구현할 수 있기 때문에 따로 작성, 수정, 상세 페이지를 분리하지 않고 모달창으로 구현해보고자 한다. 더불어 Diary 상세 페이지 또한 모달창으로 구현해보기로 했다.

효율적인 리액트 모달(react-modal) 만들기React | 세상 간단하게 모달창 만들기 (라이브러리 X)를 참고했다. 전자는 모달창 만드는 원리를 알 수 있고, 후자는 그렇게 만든 모달창은 보다 단순하게 손볼 수 있게 한다.

읽는 도중 tabIndex라는 생소한 키워드가 나와서 MDN을 찾아보니 요소의 포커스 가능 여부를 나타내는 속성이라고 한다. 주로 Tab 키를 사용하는 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정하는데, 쉽게 말해서 input 같은 요소를 focus한 상태에서 tab키를 누르면 어느 요소로 이동할지 정하는 듯. tabindex="0"와 같이 지정하며, -1 등의 음수로 지정하면 tab키를 눌러서는 접근할 수 없으나 마우스 클릭으로는 focus가 가능하게 할 수 있다고 한다.

직접 만들어 보니 의외로 CSS가 다 해먹는(?) 시스템이었다. 모달창을 감싸고 위치를 조정해주는 부분은 ModalWrapper, 모달창으로 뜰 부분은 ModalContent라고 명명하고 스타일은 아래와 같이 설정했다.

⚓Style

const ModalWrapper = styled.div`
  box-sizing: border-box;

  position: fixed;

  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  z-index: 999;

  background-color: rgba(0, 0, 0, 0.6);

  outline: 0;
`;

모달창과 구분하기 위해 배경색을 어둡게 지정하고, 이렇게 지정된 배경이 페이지 전체를 채우도록 top, bottom, left, right를 0으로 설정했다. 그리고 다른 요소들의 위에 표시되어야 하므로 z-index를 극단적으로 주었다(ㅋㅋㅋ)

const ModalContent = styled.div`
  box-sizing: border-box;

  position: relative;

  background-color: white;
  border-radius: 5px;
  box-shadow: 0 10px 15px rgb(0, 0, 0, 0.15);

  width: 60%;
  min-width: 300px;
  max-height: 80%;

  overflow: auto;

  top: 50%;
  transform: translateY(-50%);

  margin: 0 auto;
  padding: 40px;

  @media screen and (max-width: 768px) {
    width: 70%;
  }

  @media screen and (max-width: 576px) {
    padding: 20px;
  }
`;

모달창인 ModalContent는 기본 스타일로 디자인하고 반응형 디자인을 위해 min-width와 max-height를 설정했다. 그리고 내용이 길어서 max-height만큼의 모달창에 다 담을 수 없을 때를 대비하여 내용물이 넘칠 때만 스크롤이 생기도록 overflow: auto를 주었다. top, transform, margin을 이용해서 모달창을 가운데 정렬했고, 화면 크기가 작아지면 모달창의 너비와 내부 padding을 조절해서 가독성을 챙겼다.

⚓Component

const Modal = ({ children, modalHandler }) => {
  const preventPropagation = (e) => {
    e.stopPropagation();
  };

  useEffect(() => {
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = "visible";
    };
  }, []);

  return (
    <ModalWrapper tabIndex="-1" onClick={modalHandler}>
      <ModalContent tabIndex="0" onClick={preventPropagation}>
        {children}
      </ModalContent>
    </ModalWrapper>
  );

상대적으로 짧은 컴포넌트 코드. 여기서 modalHandler는 Modal이 사용될 부모 컴포넌트에서 받아 온 함수로, Modal이 열렸는지 안 열렸는지를 저장하는 state를 변경하는 함수이다.

//부모 컴포넌트
const [isModalVisible, setIsModalVisible] = useState(false);

const modalHandler = () => {
    setIsModalVisible(!isModalVisible);
  };

//jsx
<StyledBox onClick={modalHandler}>
        //코드 생략
</StyledBox>
{isModalVisible && (
  <Modal
    modalHandler={modalHandler}
    children={<DiaryDetail {...targetDiary} />}
  />
)}

Modal이 보이는지 안 보이는지를 isModalVisible이라는 state에 저장하고, 이를 변경할 수 있는 modalHandler 함수를 만든다. 그 다음, 클릭했을 때 Modal이 열렸으면 하는 요소에 onClick 이벤트로 전달하고 해당 state가 true일 경우 Modal 컴포넌트가 뜰 수 있게 조건부로 렌더링을 한다.

이렇게 하면 StyledBox를 눌렀을 때 modalHandler가 isModalVisible을 true로 변경하고, 그에 따라 Modal이 뜨게 된다. 그리고 Modal 컴포넌트에 modalHandler 함수를 전달해서 ModalWrapper와 연결시켰기 때문에 ModalWrapper를 클릭하면 isModalVisible이 false로 변경되면서 Modal 컴포넌트가 사라지게 된다.

함수를 내려주는 이유를 처음에는 이해하지 못했는데, 이렇게 modalHandler 함수를 부모와 자식이 함께 사용해야 자식 컴포넌트에서 부모 컴포넌트의 state를 변경할 수 있기 때문이었다. 아직 리액트의 모든 기능을 다 배운 것은 아니지만, 현 시점에는 props가 진짜 제일 어려운 것 같다. 이해하고 돌아섰다가도 잠깐 생각을 잘못하면 어 이게 왜 이렇게 되지...? 하는 매직...

다시 Modal 컴포넌트로 돌아와서, ModalContent가 아닌 ModalWrapper 부분을 눌렀을 때만 Modal이 꺼지도록 preventPropagation 함수를 만들어서 ModalContent에 연결했다. 이벤트 버블링 때문에 자식 요소에서 일어난 이벤트가 부모 요소를 타고 올라가서 동작하므로, ModalContent에 onClick으로 modalHandler를 연결시키지 않아도 ModalContent를 클릭하면 onClick 이벤트가 전파되어 ModalWrapper의 onClick 이벤트를 발생시키기 때문이다.
이때 e.stopPropagation 메서드를 사용하면 ModalContent를 클릭해도 onClick 이벤트가 ModalWrapper로 전파되지 않도록 할 수 있다. 그러면 모달창을 눌렀을 때는 아무런 일이 일어나지 않고, 바깥쪽 어두운 배경을 눌렀을 때만 Modal 컴포넌트가 사라진다!

마지막으로 모달창이 열렸을 때 배경이 스크롤 되는 것을 막기 위해 구글링을 해서 Modal 컴포넌트 내부에 document.body.style.overflow = "hidden";을, 모달창을 닫는 함수에 document.body.style.overflow = "visible";을 추가해 주었다. 모달창이 열렸을 때 배경이 되는 body에서 Modal 컴포넌트의 배경보다 큰, 즉 overflow 되는 부분은 감춰버리는 원리인 듯하다. body에는 overflow 속성이 따로 적용되어 있지 않았기 때문에 모달창을 닫은 후에는 overflow의 default 속성인 visible로 리셋시켰다.

그러다 이렇게 overflow 속성을 분리시킬 게 아니라 useEffect를 이용해서 Modal창이 동작할 때만 해당 속성이 적용되도록 만들어 주는 게 더 간단하겠다는 생각이 들었다.

  useEffect(() => {
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = "visible";
    };
  }, []);

return문을 사용하면 모달창이 닫혔을 때 해당 코드를 실행시킬 수 있다. 그리고 Modal이 마운트 되는 시점에만 useEffect가 실행되도록 의존성 배열을 비워둔 채로 전달했다.


⚓3. DiaryDetail

이렇게 만든 모달창에는 알맹이가 없기 때문에, DiaryBox를 클릭하면 해당 일기의 상세 페이지가 모달창으로 뜨게끔 하기 위해 모달창 내부에 넣을 자식 요소로 DiaryDetail이라는 컴포넌트를 만들어서 전달했다. 이때 클릭한 DiaryBox에 있는 데이터가 전달되어야 하므로 targetDiary에 해당 데이터를 저장한 후 이를 DiaryDetail에 props로 내려주었다!

const DiaryDetail = ({ category, content, date, title, id }) => {
  const { onRemove } = useContext(DiaryDispatchContext);

  const removeHandler = () => {
    if (window.confirm("Are you sure you want to delete it?")) {
      onRemove(id);
    }
  };
};

//onRemove
case "REMOVE": {
      newState = state.filter((item) => item.id !== action.targetId);
      break;
}

const onRemove = (targetId) => {
    dispatch({
      type: "REMOVE",
      targetId,
    });
};

Context를 이용해서 onRemove 함수를 받아오고, confirm창에서 확인을 누르면 onRemove가 실행되도록 함수를 만들었다. 이것을 delete 버튼에 연결하면, newState를 현재 Diary 데이터의 id가 아닌 데이터들만 필터링해서 새로운 배열로 만듦으로써 현재 Diary를 삭제시킬 수 있다.


⚓4. Diary 데이터 localStorage에 저장하기

처음에는 DiaryEditor의 submitHandler의 맨 마지막에 localStorage.setItem()을 쓰면 되지 않을까 생각했었다. 그런데 그렇게 되면 handler마다 그에 맞는 메서드를 가져다 붙여야 하고, 개별적으로 데이터 state를 불러와야 하는 끔찍한 일이 벌어질 것 같았다.

그래서 강의를 참고했더니 데이터를 하나의 state로 관리하는 Reducer의 마지막 부분에 localStorage 관련 메서드를 사용하면 한 번에 해결! 이미 reducer 함수가 Diary와 관련된 state를 newState라는 배열에 업데이트 시키고 있으니 업데이트된 값만 localStorage에 쏙 집어넣으면 되는 것이었다.

하지만 이게 끝은 아니다. 이렇게 reducer에서 관리되는 state를 const [data, dispatch] = useReducer(diaryReducer, []); 이러한 코드를 통해 data라는 이름을 붙여서 사용하고 있는데, 이때 초기값이 빈 배열이기 때문에 App 컴포넌트가 리렌더링 되면 data도 비어버린다. 그러므로 useEffect를 이용하여 localStorage에 저장된 데이터를 꺼내오고(getItem) 이를 data의 초기값으로 설정해 주면 된다. 초기값 설정을 위해서는 diaryreducer 함수에 미리 만들어 둔 "INIT" 액션을 사용했다.

이렇게 하면 그때그때 데이터를 불러오는 작업 없이도 Create, Edit, Remove 되어 업데이트된 데이터를 사용할 수 있다.


⚓5. 오늘의 문제

오늘의 문제는 두 개였고, 아주 귀여운 녀석부터 풀어보자면,

⚓5-1. 다이어리 수정 시 날짜

수정할 때 다이어리 작성 날짜가 원래 날짜 그대로 저장되어야 하는데 이상한 날짜로 저장되는 것을 발견했다. 알고 보니 onEdit에 전달하는 인수 중에 date를 수정하고자 하는 데이터의 원래 날짜인 originData.date로 받아오지 않아서였다. 바로 해결.

⚓5-2. 다이어리 작성 후 삭제 시 문제

localData를 불러와서 reducer의 초기값으로 전달해주는 useEffect 콜백을 짜는 과정에서 버그를 발견했다. 만들어진 Diary를 모두 삭제해서 localData로 저장된 값이 빈 배열이 됐을 경우, useEffect에서 id값이 undefined이라 읽을 수 없다는 오류가 떴다.

처음에는 localData가 있을 때만 실행되라고 했는데 왜 이런가 봤더니, 데이터가 모두 삭제 되었을 때 localStorage가 비워지는 것이 아니라 diary라는 key의 value가 빈 배열로 남아있기 때문에 localData가 falsy하지 않은 것이었다.

그래서 선택한 방법은 localData가 비어있을 경우의 length를 재서 그 이상의 길이일 때, 즉 Diary가 하나라도 저장되어 있을 경우에만 useEffect를 실행하도록 코드를 수정하였다. 콘솔창에 찍어보니 비어있을 때 localData.length는 2여서 localData.length > 2일 때만 함수가 실행되게끔 바꾸었다!

...그렇게 해결된 줄 알았는데 이번에는 Diary를 작성하기 전이라 localStorage에 빈 배열조차 남아있지 않는 상태일 때 또다시 오류가 발생했다. localData가 null이므로 length를 받아올 수 없다고...ㅋㅋ... 그래서 앞서 작성했던 조건 두 개를 합쳐서 if(localData && localData.length > 2)로 바꾸었다. 이런저런 경우의 수를 다 생각하는 건 정말 어렵다...


⚓6. 가벼운 회고

컴포넌트가 점점 늘어나고 같은 데이터를 사용하는 부분이 늘어나니까 확실히 할 일이 많아지는 듯하다. 이번에도 Modal을 만드는 것 자체는 크게 어렵지 않았는데 이걸 컴포넌트에 적용하고, 기존 데이터를 불러올 수 있게 연결하는 등의 과정이 더 힘들었으니까.

그래도 다행인 점은, 결국에는 Modal을 제대로 구현했다는 것! 그리고 Diary와 관련된 부분은 끝냈다는 것!

이제 같은 로직으로 WishList를 만들어 볼 차례인데 잘할 수 있겠지? 이게 단순히 이름만 다른 컴포넌트를 만든다고 될 일이 아니라 새로운 state를 관리할 reducer도 새로 만들어야 해서 살짝 머리 아프다. Redux를 쓰면 이런 문제도 해결 되나? 일단은 reducer를 여러 개 쓸 수 있는지부터 찾아봐야겠다. 가보자고~!

0개의 댓글