ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 컴포넌트가 무시하지 못할 정도로 크기가 커지고 있을 때, 한 번쯤은 의심해봐야 했다.
    Today I Learned 2022. 12. 7. 23:58

     

    https://innu3368.tistory.com/201

     

    간단한 알림 기능 구현하기

    현재 앱에서는 사용자가 운동에 참가를 신청하거나, 작성자가 사용자의 운동 참가 신청을 수락하는 등의 행동을 했을 때, 각 사용자가 그런 동작이 일어난 사실을 알기 위해서는 게시글을 직접

    innu3368.tistory.com

     

    오늘은 사용자가 앱 내에서 발생한 이벤트를 쉽게 확인할 수 있는 알림 기능 구현을 이어서 진행했다. 하지만 알림 기능에 대한 설명을 하기보다는, 기능을 구현하는 중간에 동료분과 홀맨님이 나눴던 이야기를 바탕으로 느낀 점을 중점적으로 적어보고자 한다.

     

     

    오늘 이전까지의 알림 기능 구현 진척도이다.

    - 특정 이벤트가 발생할 때 (운동 참가 신청, 참가 신청 수락 등) 대상자에게 알림이 전달된다.

    - 사용자는 알림 목록 조회 화면에서 모든 알림을 확인할 수 있다.

     

     

    오늘 구현한 내용은 다음과 같다.

    - 알림은 읽지 않은 상태와 읽은 상태를 가진다. 알림 상세 내용을 확인하면 읽은 상태로 전환된다.

    - 헤더에서 읽지 않은 알림의 개수를 확인할 수 있다.

    - 알림을 확인할 때 읽지 않은 알림 목록만이 조회되도록 할 수 있다.

    - 여러 개의 알림을 한번에 읽은 상태로 전환하거나 삭제할 수 있다.

     

     

    최종 목표로 하는 작업의 크기가 규모가 있는 만큼 여러 번 작업을 나눠서 진행했고, 14뽀모를 사용해 구현을 진행했다. 문제는 구현을 진행할수록 컴포넌트 안에서와, Layer 간을 왔다갔다하는 비용이 점점 커지는 것 같다는 느낌이 들었다.

     

    구현을 시작할 때와 구현을 마친 시점에서 알림 목록 화면 컴포넌트의 크기 차이를 비교해보자.

     

    // src/components/Notices.jsx
    
    export default function Notices({
      notices,
      noticesDetailState,
      showNoticeDetail,
      navigateBackward,
    }) {
      const onClickBackward = () => {
        navigateBackward();
      };
      
      const handleClickShowNoticeDetail = (targetIndex) => {
        showNoticeDetail(targetIndex);
      };
    
      return (
        <Container>
          <TopSection>
            <BackwardButton
              type="button"
              onClick={onClickBackward}
            >
              ⬅️
            </BackwardButton>
          </TopSection>
          {(!notices || notices.length === 0) ? (
            <p>조회 가능한 알림이 없습니다.</p>
          ) : (
            <NoticeList>
              {notices.map((notice, index) => (
                <>
                  <NoticeTitle key={notice.id}>
                    <button
                      type="button"
                      onClick={() => handleClickShowNoticeDetail(index)}
                    >
                      <p>{notice.createdAt}</p>
                      <p>{notice.title}</p>
                    </button>
                  </NoticeTitle>
                  {noticesDetailState[index] && (
                    <NoticeDetail>
                      <p>{notice.detail}</p>
                    </NoticeDetail>
                  )}
                </>
              ))}
            </NoticeList>
          )}
        </Container>
      );
    }
    // src/components/Notices.jsx
    
    export default function Notices({
      navigateBackward,
      notices,
      noticeStateToShow,
      showAll,
      showUnreadOnly,
      noticesDetailState,
      showNoticeDetail,
      closeNoticeDetail,
      selectNoticeState,
      toggleSelectNoticeState,
      noticesSelectedState,
      selectNotice,
      selectAllNotices,
      deselectAllNotices,
      readSelectedNotices,
      deleteSelectedNotices,
    }) {
      const onClickBackward = () => {
        navigateBackward();
      };
    
      const handleClickShowAll = () => {
        showAll();
      };
    
      const handleClickShowUnreadOnly = () => {
        showUnreadOnly();
      };
    
      const handleClickShowNoticeDetail = ({ targetIndex, targetId }) => {
        showNoticeDetail({ targetIndex, targetId });
      };
    
      const handleClickCloseNoticeDetail = (targetIndex) => {
        closeNoticeDetail(targetIndex);
      };
    
      const handleClickToggleSelectNoticeState = () => {
        toggleSelectNoticeState();
      };
    
      const handleClickSelectNotice = ({ targetIndex, targetId }) => {
        selectNotice({ targetIndex, targetId });
      };
    
      const handleClickSelectAllNotices = () => {
        selectAllNotices();
      };
    
      const handleClickDeselectAllNotices = () => {
        deselectAllNotices();
      };
    
      const handleClickReadSelectedNotices = () => {
        readSelectedNotices();
      };
    
      const handleClickDeleteSelectedNotices = () => {
        deleteSelectedNotices();
      };
    
      return (
        <Container>
          <TopSection>
            <BackwardButton
              type="button"
              onClick={onClickBackward}
            >
              ⬅️
            </BackwardButton>
            <Functions>
              <button
                type="button"
                onClick={handleClickToggleSelectNoticeState}
              >
                알림 선택
              </button>
              <button
                type="button"
                onClick={handleClickShowAll}
              >
                모든 알림 확인
              </button>
              <button
                type="button"
                onClick={handleClickShowUnreadOnly}
              >
                읽지 않은 알림만 확인
              </button>
            </Functions>
          </TopSection>
          {(!notices || notices.length === 0) ? (
            <p>조회 가능한 알림이 없습니다.</p>
          ) : (
            <NoticeList
              notices={notices}
              noticeStateToShow={noticeStateToShow}
              noticesDetailState={noticesDetailState}
              onClickShowNoticeDetail={handleClickShowNoticeDetail}
              onClickCloseNoticeDetail={handleClickCloseNoticeDetail}
              selectNoticeState={selectNoticeState}
              noticesSelectedState={noticesSelectedState}
              onClickSelectNotice={handleClickSelectNotice}
              onClickSelectAllNotices={handleClickSelectAllNotices}
              onClickDeselectAllNotices={handleClickDeselectAllNotices}
              onClickReadSelectedNotices={handleClickReadSelectedNotices}
              onClickDeleteSelectedNotices={handleClickDeleteSelectedNotices}
            />
          )}
        </Container>
      );
    }

     

    Notices 컴포넌트는 최상위 페이지 컴포넌트인 NoticesPage로부터 Store의 모든 상태 및 핸들러 함수를 props로 전달받아 알림 리스트를 출력하는 NoticeList 컴포넌트로 prop들을 다시 내려준다. 지금 방식에서는 Store에서 관리해야 할 상태나 처리할 로직, API 요청 호출이 늘어날수록 페이지 컴포넌트와 하위 컴포넌트 간에 주고받아야 하는 prop들이 위의 소스코드처럼 많아지게 된다.

     

    프론트엔드 페이지 컴포넌트 관련 작업을 하고 있던 동료분이 비슷한 상황을 겪으면서 무언가 이상했다고 생각했는지 홀맨님께 질문을 드렸고, 관련된 이야기를 들을 수 있었다. Store는 비즈니스 로직의 시작점이고, 전역으로 상태를 관리하는 게 목적이므로 필요하다면 어느 컴포넌트에서든지 직접 접근할 수 있다는 내용이었다.

     

    만약 컴포넌트에서 직접 Store에 접근하는 방식을 적극적으로 적용시킨다면, 컴포넌트를 여러 개로 나눠서 각 컴포넌트가 Store로부터 가져오는 상태 값들의 양을 적게 가져가는 식으로 리팩터링을 진행할 수 있을 것이다. 이렇게 하면 나눠진 컴포넌트가 Store로부터 전달될 데이터를 Mocking하는 크기가 적정선에서 유지될 것이기 때문에, 테스트 코드를 유지보수하는 비용이 지금보다 훨씬 가볍게 유지될 수 있을 것 같다.

     

    그리고 페이지 컴포넌트는 차상위 컴포넌트를 렌더링하는 것 외에는 페이지 이동과 관련된 작업에만 집중할 수 있게 되므로, 관심사의 분리를 실현하는 데에도 도움이 될 것이다.

     

     

    지금까지는 페이지 컴포넌트에서 반드시 Store의 모든 상태를 꺼내오고, 컴포넌트는 페이지가 꺼내온 상태를 전달받기만 하는 식으로 각 레이어가 책임을 분리하는 구조를 만들어야 한다고 생각했다. 그래서 컴포넌트의 크기가 커지더라도 그 구조를 해치지 않고 유지하려고 했다.

     

    그러나 최근 아샬님이 '테스트 코드가 정상적이지 않을 정도로 커지는 것으로부터 구조 설계가 올바르지 않게 된 것인지 의심해봐야 한다'는 이야기를 자주 주시고 있는 상황에서, 비슷한 동작을 하고 있는 코드가 점점 늘어나고 있을 때 한 번쯤 의심해보고 질문을 던졌어야 하지 않았나 하는 생각이 든다.

     

    마감까지 남은 시간이 정말 얼마 남지 않은 것은 사실이지만, 그렇다고 해서 안 좋은 구조를 지양하고 좋은 구조를 생각하는 것을 완전히 멈춰버리는 것은 결코 좋지 않은 판단인 것 같다.

     

     

     

    작업 내역

    프론트엔드

    - https://github.com/hsjkdss228/smash-frontend/pull/46 (알림 상세 보기)

    - https://github.com/hsjkdss228/smash-frontend/pull/47 (읽지 않은 알림 확인)

    - https://github.com/hsjkdss228/smash-frontend/pull/48 (알림 선택)

    - https://github.com/hsjkdss228/smash-frontend/pull/49 (알림 전체선택)

    - https://github.com/hsjkdss228/smash-frontend/pull/50 (읽은 알림으로 전환, 알림 삭제)

     

    백엔드

    - https://github.com/hsjkdss228/smash-backend/pull/32 (알림 하나를 읽은 상태로 변경)

    - https://github.com/hsjkdss228/smash-backend/pull/33 (읽지 않은 알림 개수 카운트)

    - https://github.com/hsjkdss228/smash-backend/pull/34 (알림 여러 개를 읽은 상태로 변경, 삭제)

     

     

     

     

    댓글

Designed by Tistory.