6 분 소요

Course

Wanted Pre Onboarding FE Course

과제

구현

무한 스크롤 개선

문제점

검색을 했을 때 이 전에 있던 검색 결과가 한번 렌더링된 후에 새로 검색된 결과가 나타났다.
또한, 검색했을 때 page가 한번 넘어가서 결과가 처음부터 보이지 않는 현상이 있었다.

의심 부분

처음에 아래와 같이 구현했었다. intersect하는 구간에서 next page를 불러오는 부분을
컴포넌트 내 state로 관리해서 검색 결과가 있을 때 다른 것을 검색했을 때 page가 건너뛰어지는 것 같다고 판단했다.

useEffect(() => {
  const onIntersect = async ([entry], observer) => {
    // 겹침 유무 확인 => 겹칠 경우에만 실행
    if (entry.isIntersecting) {
      // 겹쳤다면 잠시 관찰 종료
      observer.unobserve(entry.target);
      // **next page (그 다음 데이터 불러오기 부분)**
      setPage((prev) => prev + 1);
      // 다시 관찰 시작
      observer.observe(entry.target);
    }
  };

  // observer 생성
  let observer;
  if (target) {
    observer = new IntersectionObserver(onIntersect, {
      threshold: 0.4,
    });
    observer.observe(target);
  }

  // unmount 시 관찰 멈추기
  return () => observer && observer.disconnect();
}, [target]);

해결

따라서 page를 store에서 관리하도록 변경했다.
컴포넌트에서는 page를 1씩 올려주는 역할만하고, 나머지 연산(page를 set하거나 item을 page 별로 나누거나)은 reducer에서 하도록 했다.

// reducers
  loadMore: (state, action) => {
     // 컴포넌트에서 +1을 해서 준 page
    const page = action.payload
    const { data } = current(state)
    // 전체 items을 10개씩 쪼갬
    const pageItems = data?.items.slice((page - 1) * 10, page * 10)
    // +1된 page를 전역 상태에 반영
    state.page = page
    // 가장 마지막 페이지를 계산해서 전역 상태에 반영
    state.maxPage = Math.ceil(data?.items.length / 10)
    // 무한 스크롤이므로 10개씩 기존 배열 뒤에 갖다 붙임
    state.pageItems = [...state.pageItems, ...pageItems]
  },

이렇게 함으로써 page가 건너뛰는 현상은 해결되었다.

추가적으로, 검색 결과가 있는 상태에서 또 검색했을 때 기존 검색 결과를 다시 보여준 다음에
새로운 검색 결과를 보여주는 현상은 data를 담는 상태를 한 번 클리어해주어야 한다고 생각했다.

따라서 다음과 같이 했다.

    .addCase(fetchRepos.fulfilled, (state, action) => {
      state.loading = false
      state.page = 0
      state.pageItems = []
      state.data = [] // data 넣기 전에 클리어
      state.data = action.payload
    })

data를 이전 것을 먼저 비워 준 후 다시 새로 fetch한 data를 할당하니 이전 결과는 보이지 않게 되었다.

구현 결과
이미지

feedback 추가

repo를 추가하고 삭제할 때 사용자에게 알려주기 위해 feedback을 추가하고자 했고, 이를 위한 전역 state와 reducer를 만들었다.

store state

const initialFeedback = {
  type: "",
  msg: "",
};

const initialState = {
  // 생략
  feedback: initialFeedback, // 추가
};

type에는 success, failure, notice 등등이 들어갈 것이다.
msg는 사용자에게 보여주고 싶은 메시지를 할당할 것이다.

reducer

// reducers
    addRepoToStorage: (state, action) => {
      const repo = action.payload
      const reposFromLocal = JSON.parse(localStorage.getItem("repos"))
      const { savedRepos } = current(state)

      if (reposFromLocal.find((el) => el.id === repo.id)) {
        state.feedback = {
          type: "failure",
          msg: "이미 추가하신 repo입니다.",
        }
        return
      } else if (reposFromLocal.length >= 4) {
        state.feedback = {
          type: "failure",
          msg: "최대 저장 repo를 초과하였습니다. (최대 4개)",
        }
        return
      }

      localStorage.setItem("repos", JSON.stringify([...reposFromLocal, repo]))
      state.savedRepos = [...savedRepos, repo]
      state.feedback = {
        type: "success",
        msg: "repo를 추가하였습니다.",
      }
    },
    deleteRepoFromStorage: (state, action) => {
      const { id } = action.payload
      const filteredRepos = JSON.parse(localStorage.getItem("repos")).filter((repo) => repo.id !== id)

      localStorage.setItem("repos", JSON.stringify(filteredRepos))
      state.savedRepos = filteredRepos
      state.feedback = {
        type: "success",
        msg: "repo를 삭제하였습니다.",
      }
    },

위와 같이 추가하거나 삭제 시 아이템과 선택된 id를 비교하여 중복된 repo 인지 혹은 배열이 저장 범위를 초과하였는지 등을 확인했다.

사용 컴포넌트

// 해결 X
useEffect(() => {
  if (!feedback.msg) return;
  alert(feedback.msg);
  return () => dispatch(cleanupFeedback());
}, [feedback, dispatch]);

useEffect로 구현하다 보니 unmount될 때에도 alert가 동작되어 필요없는 피드백이 켜졌다. 그래서 cleanup 함수를 추가했다.

그러나 계속해서 나타남

console 로 계속 확인해본 결과 라우팅이 되어 컴포넌트가 새로 올라올 때 이미 store에 저장되어 있던
feedback이 한 번 alert가 뜰 수 밖에 없었다.

왜냐하면, early return은 오로지 feedback msg가 비어있을 때만 return 하는데, 그 전에 feedback이 세팅이 된 상태에서
컴포넌트가 mount된다면 early return 문을 지나서 alert이 한 번 발생하게 되었다.

따라서, 다음과 같이 cleanup을 alert 뜨기 전에 한번 해주었다.

// 해결
useEffect(() => {
  dispatch(cleanupFeedback());
  if (!feedback.msg) return;
  alert(feedback.msg);
}, [feedback, dispatch]);

해결되었다!

Pagination 구현

설계

  • 결과물을 6개씩 자르기
  • 페이지 네비게이션 양쪽에 < >으로 움직이기
  • 가운데 숫자는 3개가 보이고 나머지는 …으로 처리
  • 1 페이지와 가장 마지막 페이지는 무조건 보이도록 처리

구현

배열을 6개씩 잘라서 보여주려면 현재 페이지와 배열이 기본적으로 필요하다.

// reducers
  showCurrentRepo: (state) => {
    // 선택한 repo를 localStorage에 저장해두었다.(새로고침해도 확인 가능하도록)
    const selectedRepo = JSON.parse(localStorage.getItem("selectedRepo"))
    // 꺼내온 repo와 issues 정보들을 전역 상태에 반영
    state.repo = selectedRepo.repo
    state.issues = selectedRepo.issues
    // 가장 마지막 페이지 계산
    state.totalPage = Math.ceil(selectedRepo.issues.length / 6)
    // 쪼개서 화면에 보여질 items 계산
    state.pageItems = splitIssuesByPage(selectedRepo.issues, state.page, state)
  },

배열을 쪼개는 함수

showCurrentRepo 뿐만 아니라 다른 reducer에도 필요한 부분이 많아 함수로 따로 만들었다.

const splitIssuesByPage = (items, page, state) => {
  // 기본적으로 page에 따라 6개씩 자른다.
  const pageItems = items.slice((page - 1) * 6, page * 6);

  const { totalPage } = current(state);
  // page가 계산했던 가장 마지막 페이지보다 커지면 안된다.
  // 또한, 1보다 작아지면 안된다.
  // 따라서 totalPage를 가져와서 다음과 같이 if문을 구성한다.
  if (page >= state.totalPage) {
    // totalPage보다 커지지 못하도록 set 해준다.
    state.page = state.totalPage;
    // 가장 마지막 페이지는 마지막 요소 전 6개를 보여준다.
    return items.slice((totalPage - 1) * 6, totalPage * 6);
  } else if (page <= 1) {
    // 1보다 작아지지 못하도록 계속 1로 set 해준다.
    state.page = 1;
    // 첫번째 페이지는 제일 첫번째 부터 6번째까지만 보여준다.
    return items.slice(0, 6);
  } else {
    state.page = page;
    return pageItems;
  }
};

page를 옮기는 것은 더 쉽다.
page만 바꿔주면 splitIssuesByPage에서 알아서 쪼개서 게시할 pageItems을 연산해준다.

// reducers
    movePage: (state, action) => {
      const page = action.payload
      state.pageItems = splitIssuesByPage(state.issues, page, state)
    },
// 실제 사용 컴포넌트
const goPrev = () => dispatch(movePage(page - 1));

const goNext = () => dispatch(movePage(page + 1));

그 다음, page navigation에서 숫자를 어떻게 보여줄지 고민을 했다.

pageNumbers는 [1, 2, 3, 4 …] totalPage 만큼 들어있다.

그래서 나의 현재 페이지에서 양 옆(왼쪽, 오른쪽)은 항상 숫자가 보이도록 해야하므로
(el >= page - 1 && el <= page + 1)이러한 조건을 추가한다.
추가로 나는 가장 첫번째 페이지(1)과 가장 마지막 페이지는 항상 보이도록 설정할 것이기 때문에 el === 1 || el === totalPage 이 조건 또한 추가한다.

이제 세 개의 숫자 양쪽에 …을 붙이려면 왼쪽에는 el === page - 2 && el !== 1,
오른쪽에는 el === page + 2 && el !== totalPage의 조건을 추가하여 …을 표시한다.

el === page - 2 / el === page + 2 이 조건 모두 현재 페이지의 두 칸 옆쪽 페이지인데, 이 때부터 …을 나타내도록 하는 것이다.
하지만, 두 칸 옆쪽이 만약 1이거나 가장 마지막 페이지라면?
따라서 el !== 1 / el !== totalPage 조건을 추가한다.

<PagesWrapper>
  {Array.from(pageNumbers, (el) => (
    <PageNumberBox key={el}>
      {el === page - 2 && el !== 1 && <Dots>...</Dots>}
      {((el >= page - 1 && el <= page + 1) || el === 1 || el === totalPage) && (
        <PageNumber isActive={page === el} onClick={() => goThatPage(el)}>
          {el}
        </PageNumber>
      )}
      {el === page + 2 && el !== totalPage && <Dots>...</Dots>}
    </PageNumberBox>
  ))}
</PagesWrapper>

구현 결과
이미지

페이지네이션.. 생각보다 너무 재미있었다.

TODO

레포 추가 제한 (4개) o
레포 추가 시 같은 레포 block o
레포 추가 및 삭제 시 alert o

레포 추가 후 다시 메인으로 갔을 때 load more 시 다시 처음부터 불러오는 현상 개선 o

레포 보관함에 있는 레포 선택 시 모든 issue 불러오기 o
issue 클릭 시 url 연결 o
issue pagenation 적용 o

검색결과 없음 추가 o

repoCard language 추가 o

렌더링 최적화
api 호출 최적화

시간되면
스켈레톤 UI
피드백 alert에서 toast msg로 변경
UI 꾸미기
코드 이쁘게 정리

회고 (TIL)

2022.03.21 Daily 회고

✏오늘 한 일

  • 개인 과제 설계 및 수행

⁉느낀 점

팀 프로젝트로 했었으면 어떤 부분을 했었을까 생각해본다.
그래도 오랜만에 처음부터 끝까지 하려고 하니 재미는 있다.

확실히 팀 프로젝트로 많이 성장한 것 같다. 그 전에는 고려조차 하지 않았을 것들(렌더링 최적화, api 호출 최적화 등) 같은 것들을 생각하는 것을 보니
확실히 보는 시야가 조금은 넓어졌다고 해야하지 않을까 생각한다.

팀 프로젝트도 이런데 입사하게되면 새로운 세상이 열리겠구나.

🎃현재 나의 상태

서류가 많이 떨어지지만 뛰어난 사람이 많으니 어쩔 수 없는 것으로 위안을 삼는다.
그나마 조금 괜찮은데 들어가서 경력을 쌓는 게 먼저라는 생각이 든다!


댓글남기기