[회고] github-issues-reader

초코침·2023년 9월 11일
1

회고

목록 보기
1/2
post-thumbnail

아쉬웠던 점을 보완하여 새로 블로깅했습니다.
👉🏻 github-issues-reader를 보강해 보자

과제 소개

이번 과제는 facebook의 react 레포지토리의 issue 목록과 상세 내용을 보여주는 웹 사이트를 만드는 것이었다.

(레포 바로가기)

요구사항

  • 이슈 목록 및 상세 화면 구현
    • 목록은 open된 이슈를 코멘트가 많은 순으로 표시
    • 다섯 번째 셀마다 광고 이미지 출력
      • 광고 이미지 클릭 시 원티드 사이트로 이동
    • 이슈 목록은 인피니티 스크롤로 구현
  • 데이터 요청 중 로딩 표시
  • 에러 화면 구현

과제 회고

octokit 경험과 바보짓

이번 과제에서는 github api를 사용해야 했기에 github api를 편리하게 사용할 수 있는 octokit 라이브러리를 설치해 사용했다.

사용법은 axios로 요청을 보내는 것과 비슷한 느낌이었고, 간단한 코드로 내가 원하는 조건에 맞는 이슈들을 받아올 수 있어 편리했다.

octokitInstance()
	.request(ENDPOINT, {
		owner: REPO.OWNER,
		repo: REPO.NAME,
		page: pageNumber,
		per_page: PER_PAGE,
		state: 'open',
		sort: 'comments',
})

그런데 응답받은 페이지가 마지막 페이지인지(또는 다음 페이지가 있는지) 여부를 응답에서 확인하지 못했다. 그래서 직접 api를 찔러보고 판단하는 코드를 작성했다.

과제를 제출한 뒤에, request에 page 관련 옵션도 받는데 hasNextPage가 없는게 아무리 생각해도 이상해서 공식 문서를 꼼꼼히 읽어봤더니 header에 link라는 프로퍼티로 응답을 준다고 적혀 있었다.

(이렇게 줄 거라고는 생각도 못했어서 응답 헤더를 여러 번 읽었음에도 알아채지 못했다..)

라이브러리든 프레임워크든 어떤 기술을 사용할 때에는 내 생각을 정답이라 단정짓지 말고 꼭 공식 문서를 통해 검증해 가며 사용해야겠다.

useEffect로 연속적인 변화 일으키지 않기

과제 리뷰 세션에서 useEffect로 연속적인 변화를 일으키는 코드를 지양해야 한다는 주제를 다뤘는데 딱 내가 코드를 짜면서 고민했던 부분이라 강의가 끝나자마자 리팩토링했다..

이건 내가 작성한 이슈 목록을 페칭하는 로직을 담은 커스텀훅이다.

const useIssues = () => {
  const [issues, setIssues] = useState([]);
  const [pageNumber, setPageNumber] = useState(1);
  const [isFetching, setIsFetching] = useState(false);

  const fetchIssues = useCallback((pageNumber: number) => {
    const response = getIssues(pageNumber);
    setIssues(response.data);
    setIsFetching(false);
  }, []);

  const fetchMoreIssues = () => {
    setPageNumber(pageNumber + 1);
  };

  useEffect(() => {
    setIsFetching(true);
    fetchIssues(pageNumber);
  }, [pageNumber, fetchIssues]);

  return { issues, isFetching, fetchMoreIssues };
};

스크롤을 내려 다음 페이지를 페칭해야 할 때 호출할 fetchMoreIssues함수를 만들었다.

그런데 저 함수는 이름과는 달리 페치하는 로직을 갖고 있지 않으며, pageNumber를 변경하면서 발생하는 effect로 다음 페이지를 페칭한다.

작성하면서도 찝찝한 코드였는데, 이 코드가 바로 effect를 잘못 사용하는 예시 중 하나였다.


리액트 공식 문서를 보면 effect를 잘못 사용하는 예시에 대해 자세히 설명되어 있다. 그래서 공식 문서를 꼼꼼히 정독하면서 effect의 올바른 사용법에 대해 이해하는 시간을 가졌다. (블로깅까쥐!)

내 코드의 경우 Chains of computations에 해당하는 코드였는데, 이런 코드가 작성된 배경은 보통 업데이트된 상태를 사용하기 위함이라고 한다. 해결 방법은 매우 간단하다. effect의 콜백에 넣은 로직을 상태를 업데이트하는 곳에 함께 작성하면 된다.

나도 setPageNumber를 호출하면서 변경한 새로운 상태로 fetchIssues를 호출하고 싶었던 것이기 때문에 fetchMoreIssues 함수 내부에 effect 콜백에 작성한 로직을 가져오는 방향으로 리팩토링을 했다.

  const fetchMoreIssues = () => {
    setIsFetching(true);
    setPageNumber(pageNumber + 1);
		fetchIssues(pageNumber + 1);
  };

  useEffect(() => {
    setIsFetching(true);
    fetchIssues(1);
  }, [fetchIssues]);

이렇게 작성하면 effect는 첫 렌더링 때 필요한 1 페이지를 불러오는 역할을 하고, 추가적인 페치가 필요할 땐 fetchMoreIssues가 그 기능을 수행하게 된다. 불필요한 리렌더링을 줄이고 좀 더 이해하기 쉬운 코드가 되었다!


이번에 이 부분을 리팩토링하면서 effect가 언제 사용해야 하는 것인지도 모르고 남용하고 있었음을 깨달았다. 그래서 공식 문서를 직접 해석하며 열심히 읽었고, 읽다보니 내가 궁금했던 점들이 바로 바로 해결되는 짜릿한 경험을 했다. 뿐만 아니라 리팩토링도 쉽게 끝낼 수 있었다. (그리고 리액트 공식 문서 읽는게 재밌어져서 스터디도 하나 새로 들었다🤭)

fetch할 때 필요한 여러 상태를 효율적으로 다루는 방법

무한 스크롤을 위한 fetch 로직을 작성하기 위해 isFetching, datas, hasNextPage 등의 상태를 개별적으로 뒀다.

const useIssues = () => {
  const [issues, setIssues] = useState([]);
  const [pageNumber, setPageNumber] = useState(1);
  const [isFetching, setIsFetching] = useState(false);

	// ...

그런데 이렇게 서로 연관있는 상태는 하나로 묶어서 관리하는 게 관심사의 분리 측면에서 좋다.

(fetch 중일 때는 intersection observer가 작동하지 않도록 코드를 짰는데, fetch하기 전에 setIsFetching을 호출하지 않아 무한으로 fetch 호출을 한 적이 있다..)


이때 useReducer를 사용하면 연관 있는 상태를 묶어 업데이트하기 편리하다.

const [{ issues, isLoading, hasNextPage }, dispatch] = useReducer(issueReducer, {
	issues: [],
	isLoading: false,
	hasNextPage: false,
});
const responseData = await getIssues(pageNumber, ISSUE_COUNTS_PER_PAGE);
// 🟢 연관 있는 상태를 한 번에 업데이트!
dispatch({
	type: 'loadIssue',
	issues: responseData.data,
	hasNextPage: responseData.hasNextPage
});
const issueReducer = (
  state: IssueQueryState,
  action:
    | {
        type: 'loadIssue';
        issues: IssuesResponseData;
        hasNextPage: boolean;
      }
    | {
        type: 'requestIssue';
      },
): IssueQueryState => {
  switch (action.type) {
    case 'requestIssue': {
      return { ...state, isLoading: true };
    }
    case 'loadIssue': {
      return {
        ...state,
        issues: [...state.issues, ...action.issues!],
        isLoading: false,
        hasNextPage: action.hasNextPage!,
      };
    }
    default: {
      throw new Error();
    }
  }
};

isLoading 같은 상태를 둘 때마다 페칭 전에 true로 바꾸고 페칭 끝나면 false로 바꾸는 걸 일일이 작성하기 번거로웠는데, 이렇게 리듀서를 사용하면 가독성도 좋고 무엇보다 상태 업데이트를 빠뜨리는 실수를 방지해줄 수 있다!

profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글