[Wanted]_Week4-1_3주차 과제 피드

hanseungjune·2023년 7월 20일
0

Wanted

목록 보기
17/21
post-thumbnail

custom hook에서는 동등성 보장한 상태로 리턴해주기

  • Good
// 이슈들을 관리하는 context provider 컴포넌트를 정의합니다.
export function IssueContextProvider({ children }: IssueContextProviderProps) {

  // 이슈 데이터를 state로 관리합니다. 초기값은 빈 배열입니다.
  const [issues, setIssues] = useState<IssueListResponseType['data']>([]);
  
  // 로딩 상태를 state로 관리합니다. 초기값은 false입니다.
  const [isLoading, setIsLoading] = useState(false);

  // 에러 상태를 state로 관리합니다. 초기값은 false입니다.
  const [isError, setIsError] = useState(false);

  // 현재 페이지 번호를 참조하는 ref입니다. 초기값은 1입니다.
  const pageRef = useRef(1);
  
  // 더 이상 가져올 데이터가 없는지 여부를 참조하는 ref입니다. 초기값은 false입니다.
  const isEndRef = useRef(false);

  // 무한 스크롤로 이슈를 더 가져오는 함수입니다.
  const getInfiniteIssues = useCallback(async () => {
    // 더 이상 가져올 데이터가 없으면 함수를 종료합니다.
    if (isEndRef.current) return;

    setIsLoading(true); // 로딩 상태를 true로 변경합니다.
    try {
      const response = await RepositoryAPI.getIssueList(pageRef.current); // API로부터 이슈 리스트를 가져옵니다.
      if (response.length === 0) {
        isEndRef.current = true; // 가져올 데이터가 더 이상 없으면 isEndRef를 true로 변경합니다.
        return;
      }

      pageRef.current = pageRef.current + 1; // 페이지 번호를 증가시킵니다.
      setIssues((prev) => [...prev, ...response]); // 이전 이슈 데이터에 새로운 이슈 데이터를 추가합니다.
    } catch (error) {
      setIsError(true); // 에러가 발생하면 에러 상태를 true로 변경합니다.
    } finally {
      setIsLoading(false); // 함수가 끝나면 로딩 상태를 false로 변경합니다.
    }
  }, [RepositoryAPI]); // RepositoryAPI가 변경될 때마다 함수를 새로 생성합니다.

  // 제공할 context 값을 설정하고 children을 렌더링합니다.
  return (
    <IssueContext.Provider
      value={{ issues, isLoading, getInfiniteIssues, isError }}
    >
      {children}
    </IssueContext.Provider>
  );
}

의존성 드러내주기

  • Good
// 주어진 id에 해당하는 이슈를 가져와 상태를 설정하는 비동기 함수를 정의합니다.
const fetchIssue = async (id: string) => {
  let response;

  try {
    if (id) { // id가 유효한 경우에만
      response = await RepositoryAPI.getIssue(id); // API로부터 이슈를 가져옵니다.
      setIssue(response); // 가져온 이슈를 상태에 설정합니다.
      setLoading(false); // 로딩 상태를 false로 설정합니다.
    }
  } catch (error) {
    return navigate('/error'); // 에러가 발생하면 '/error' 페이지로 이동합니다.
  }
};

// useEffect 훅을 사용하여 컴포넌트가 마운트되거나 id가 변경될 때마다 이슈를 가져옵니다.
useEffect(() => {
  if (id) { // id가 유효한 경우에만
    fetchIssue(id); // 해당 id의 이슈를 가져옵니다.
  }
}, [id]); // id가 변경될 때마다 이 훅을 실행합니다.

Try, Catch, Finally 속성 잘 이해하고 사용하기

  • Good
// 이슈 목록을 가져오는 비동기 함수를 정의합니다.
const fetchIssueList = async () => {
  try {
    const currentPage = issueListPage; // 현재 페이지 번호를 가져옵니다.
    handleLoading(true); // 로딩 상태를 true로 설정합니다.

    // API로부터 현재 페이지의 이슈 목록을 가져옵니다. 한 페이지당 10개의 이슈를 가져옵니다.
    const newIssueList = await getIssueList(currentPage, 10);

    setIssueListPage((prev) => prev + 1); // 페이지 번호를 증가시킵니다.
    setIssueList((prevList) => [...prevList, ...newIssueList]); // 이전 이슈 목록에 새로운 이슈 목록을 추가합니다.
  } catch (error) {
    // 에러가 발생하면 에러 메시지를 상태에 설정합니다.
    const err = error as SystemError;
    setFetchError(err.message);
  } finally {
    // 함수의 마지막에 로딩 상태를 false로 설정합니다.
    handleLoading(false);
  }
};

코드의 결합은 신중하게 그리고 최대한 느슨하게

  • Good
// HttpClient 클래스는 API 통신을 위한 기본 설정을 담당합니다.
class HttpClient {
  // GitHub API의 기본 URL
  private BASE_URL = "<https://api.github.com>";

  // 인증 토큰 값, 환경변수에서 불러옵니다.
  private TOKEN = process.env.REACT_APP_AUTHORIZATION;

  // Axios 라이브러리를 사용해 API 호출을 수행합니다.
  // baseURL과 헤더에 인증 토큰을 설정하여 인스턴스를 생성합니다.
  protected axiosInstance = axios.create({
    baseURL: this.BASE_URL,
    headers: {
      Authorization: this.TOKEN,
    },
  });
}

// IssueService 클래스는 GitHub의 Issue에 관련된 API 요청을 담당합니다.
export class IssueService implements IssueServiceType {
  // HttpClient 인스턴스를 저장할 변수
  private httpClient: HttpClient;

  // 생성자에서 HttpClient 인스턴스를 받아 초기화합니다.
  constructor(httpClient) {
    this.httpClient = httpClient;
  }

  // 특정 페이지의 Issue 목록을 가져옵니다.
  async getIssueList(pageNum: number) {
    return await this.httpClient.axiosInstance
      .get(
        `/repos/facebook/react/issues?state=open&sort=comments&direction=desc&per_page=12&page=${pageNum}`
      )
      .then((response) => {
        // HTTP 상태 코드를 확인합니다.
        const status = response.status;
        if (status === 200) {
          // 데이터가 비어있지 않은 경우, 데이터를 추출합니다.
          const isEmpty = response.data.length === 0;
          return !isEmpty ? extractIssueList(response.data) : false;
        }
        // 상태 코드에 따른 알림을 보여줍니다.
        alertStatus(status, GET_ISSUE_LIST_STATUS);
      })
      .catch((error) => {
        // 에러 발생 시 콘솔에 로그를 출력합니다.
        console.error("Error:", error);
      });
  }

  // 특정 id의 Issue 상세 정보를 가져옵니다.
  async getIssueDetail(id: number) {
    return await this.httpClient.axiosInstance
      .get(`/repos/facebook/react/issues/${id}`)
      .then((response) => {
        const status = response.status;
        // HTTP 상태 코드를 확인하고, 200인 경우 데이터를 추출합니다.
        if (status === 200) return extractIssueDetail(response.data);
        // 상태 코드에 따른 알림을 보여줍니다.
        alertStatus(status, GET_ISSUE_STATUS);
      })
      .catch((error) => {
        // 에러 발생 시 콘솔에 로그를 출력합니다.
        console.error("Error:", error);
      });
  }
}
  • 상속은 가장 강한 형태의 결합, 코드가 상호 결합된다는 것은 한쪽의 변화가 다른쪽에 영향을 미치게 된다는 것, 따라서 상속은 신중하게 사용해야 함

Class 내부에서만 사용하는 상수는 Class안으로 넣어주기

  • Good
// IssueApi 클래스는 GitHub의 facebook/react 리포지토리와 관련된 API 요청을 담당합니다.
export class IssueApi {
  // HttpClient 인스턴스를 저장할 변수
  private httpClient: HttpClient;

  // GitHub의 facebook/react 리포지토리와 관련된 API 요청 URL
  private readonly URL = '/repos/facebook/react';

  // 생성자에서 HttpClient 인스턴스를 받아 초기화합니다.
  constructor(httpClient: HttpClient) {
    this.httpClient = httpClient;
  }

  // 리포지토리의 정보를 가져옵니다.
  async repository() {
    // httpClient의 fetch 함수를 이용해 GET 요청을 보냅니다.
    const response = await this.httpClient.fetch(this.URL, {
      method: 'GET',
    });

    // 응답을 JSON 형태로 파싱하여 반환합니다.
    return response.json();
  }
}
  • 클래스는 내부에 데이터를 가지고 있을 수 있기에, 굳이 바깥 범위인 파일에 놔두는 것 보다 내부에서만 사용되는 값이라면 내부로 가져와서 저장해두는 것이 효율적
  • 이 과정에서
    • 수정되지 않는 상수이기에 readonly,
    • 외부로 노출될 필요가 없기에 private

CSS-in-JS와 className을 이용한 스타일링 혼용 금지

  • bad
const IssueUlStyle = styled.ul`
  width: 100%;
  padding: 16px;
  overflow-y: auto;
  height: calc(100% - 62px);
  position: relative;

  .imptyImg {
    width: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
`;
  • styled-component를 이용해서 스타일링이 된 태그들은 해당 definition으로 이동해서 바로 스타일링을 확인이 가능한데 className으로 선언한 부분은 바로 definition으로 이동이 불가능해서 스타일링 파악하기에 어려움이 있음
  • 위의 내용에 덧붙여서 className을 이용한 스타일링의 nesting depth가 깊어지는 경우에는 더 더욱 찾기가 힘들어짐
  • depth가 깊어지는 경우 뿐만 아니라 1depth로도 className을 이용한 스타일 선언부가 길어지는 경우에도 해당 스타일링을 찾기 어려워짐
  • 위의 사항들로 인해서 CSS in JS(styled-components 등)을 이용한 스타일링을 하는 경우에는 라이브러리 사용상 강제되는 등의 불가피한 부분을 제외하고는 className을 통한 스타일링은 최대한 지양하는게 권장됩니다.

tag selector 사용 지양

  • bad
const IssueLiStyle = styled.li`
  padding: 15px 10px;
  border-radius: 10px;

  a {
    display: flex;
    justify-content: space-between;
    align-items: center;

    img {
      background-color: #fff;
      height: 100%;
      padding: 0 35%;
    }
  }

  &:hover {
    background-color: ${COLOR.DarkHover};

    h3 {
      text-decoration: underline;
      color: ${COLOR.White};
    }
  }
`;
  • a, img 태그 처럼 빈번히 사용되는 태그로 스타일을 주는 건 depth에 무관하게 적용되어 의도치 않은 스타일링이 될 수 있습니다.
  • tag selector는 해당 요소 뿐만 아니라 동일한 태그로 선언된 다른 요소들에게도 영향을 미칠 여지가 너무 많기 때문에 추후 유지보수에 악영향을 미칠 수 있습니다.

의미가 드러나지 않는 값들 대신에 의미를 명확하게 알려주기

  • good
const isAdvertisement = isItMultipleOfFive(index+1);
  isAdvertisement &&
    nodes.push(
    <li key={`ad#${index}`}>
    	<Advertisement />
    </li>,
  	);
  return nodes;

너무 많은 정보를 주는 것은 추상화가 제대로 되지 않은 것

  • bad
function IssueList() {
const {
  issueList: data,
  isEnd,
  countLoading,
  fetchIssues,
  fetchMoreIssues,
  fetchIssueCount,
  isLoading,
} = useIssues();
const target = useRef(null);
const [page, setPage] = useState(1);
  • good
function IssueList() {
  const { data, error, loading, hasNextPage } = useIssueList();
  const fetchNextPage = useIssueListDispatch();
  const observeTargetRef = useInfiniteIssue<HTMLDivElement>();

동등성 제대로 보장해주기

  • good
// useIssues 라는 훅을 사용해, 이슈들과 관련된 데이터 및 함수들을 가져옵니다.
const { issues, getIssues, error, loading, hasNextPage } = useIssues();

// useCallback을 이용해 getMoreIssues 함수를 메모이제이션합니다.
// 여기서 getIssues, hasNextPage, loading이 변경될 때만 새로운 함수를 생성하도록 합니다.
// 이 함수는 Intersection Observer의 callback으로 사용됩니다.
// 즉, 특정 요소가 화면에 보이게 될 때 이 함수가 실행됩니다.
// hasNextPage가 true이고 현재 loading 중이 아니라면, getIssues를 호출해 새로운 이슈들을 불러옵니다.
const getMoreIssues = useCallback(
  async (entry, observer) => {
    // 이전에 관찰하던 요소의 관찰을 중지합니다.
    observer.unobserve(entry.target);
    // 다음 페이지가 있고 현재 로딩 중이 아니라면
    if (hasNextPage && !loading) {
      // getIssues를 호출해 새로운 이슈들을 불러옵니다.
      getIssues();
    }
  },
  // 의존성 배열에 getIssues, hasNextPage, loading을 지정해
  // 이 값들이 변경될 때만 새로운 함수를 생성하도록 합니다.
  [getIssues, hasNextPage, loading]
);

// useObserver는 Intersection Observer 훅으로
// 위에서 정의한 getMoreIssues 함수를 인자로 전달받아,
// 특정 요소가 화면에 보이게 될 때 이 함수를 실행하도록 합니다.
const ref = useObserver(getMoreIssues);

의존성 잘 넣어주기

  • good
const DetailPage = () => {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();

  if (!id) {
    navigate(PATH.ERROR_PAGE);
    alert('페이지를 찾을 수 없습니다!');
  }

  const { detail, loading, loadIssueDetail } = useIssueDetail();
  const { number, title, body, comments, updated_at, user } = detail;

  // 해당 코드에 의존성을 잘 넣어줘야한다는 말이다.
  useEffect(() => {
    loadIssueDetail(Number(id));
  }, [id, loadIssueDetail]);
profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글