이번에는 Github API를 활용하여 facebook/react 의 issues를 클론하는 작업을 해보았습니다.
이번 과제에서 중요했던 점은 API와 Context를 연동해야 한다는 점이었습니다.
context와 custom Hook를 활용한 issues, issue 전역상태로 관리
❓선정 이유
// useIssue.ts
// import 생략
const pathParam: GetIssuePathParam = {
repo: 'react',
owner: 'facebook',
issue_number: 0,
};
export function useIssue() {
const context = useContext(IssueContext);
if (!context) throw new Error('IssueContextProvider를 찾을 수 없습니다!');
const { issue, setIssue } = context;
const { issueList } = useIssues();
const [isLoading, setIsLoading] = useState(false);
const fetchIssue = async (issueNumber: number) => {
if (!!issue && issue.number === issueNumber) {
return;
}
for (const issue of issueList) {
if (issue.number === issueNumber) {
setIssue(issue);
return;
}
}
setIsLoading(true);
const res = await getIssue({ ...pathParam, issue_number: issueNumber });
setIssue(res);
setIsLoading(false);
};
API 요청
❓선정이유
type QueryParam = {
[key: string]: string | number; // primitive type
};
const makeQueryString = (object: QueryParam) => {
const querystring = [];
for (const key in object) {
querystring.push(`${key}=${object[key]}`);
}
return querystring.join('&');
};
export { makeQueryString };
❓선정이유
freeze
를 이용하여 외부에서 접근하여 변경하려는 시도를 방어하였습니다.IntersectionObserver
을 이용하여 스크롤을 내리면 이슈 목록을 추가적으로 로딩하였습니다. 사용자가 스크롤을 조금씩 내리면서 이슈를 로딩하므로 초기 로딩 시간을 단축시킬 수 있고, 전체 이슈 목록을 한 번에 로딩하는 것보다 자원을 효율적으로 사용할 수 있습니다.react-markdown
을 이용하여 이슈에 적힌 마크다운 문법을 렌더링 했습니다. 따라서 사용자가 이슈 내용을 보다 직관적이고 가독성 있게 확인할 수 있습니다.// pathParam.ts
import { GetIssuesPathParam } from '../types/issuesApi';
const pathParam: GetIssuesPathParam = { repo: 'react', owner: 'facebook' };
export default Object.freeze(pathParam);
// useIntersectionObserver.ts
import { useRef } from 'react';
export default function useIntersectionObserver(callback: () => void) {
const observer = useRef(
new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
callback();
}
});
},
{ threshold: 1 },
),
);
const observe = (element: HTMLElement | null) => {
element && observer.current.observe(element);
};
const unobserve = (element: HTMLElement | null) => {
element && observer.current.unobserve(element);
};
return [observe, unobserve];
}
❓선정이유
Client side에서 404(NotFoundPage) 에러가 발생했을 때 뿐만 아니라, api 요청 중 발생할 수 있는 에러들에 대해 핸들링함으로서, 에러 발생에 방어적으로 대응하는 코드가 작성된 것을 선정했습니다.
현재 아래의 에러들에 대해 핸들링 돼있습니다.
/hello
issues/99999
에러를 전역적으로 관리할 것인지, 지역적으로 관리할 것인지에 대한 의견이 나뉘었습니다. 첫 번째 방식은 반복된 상태와 컴포넌트를 선언하지않아 코드 품질을 향상합니다. 두 번째 방식은 사용자 경험을 고려해 깨진 UI 일부분만을 보여준다는 장점이 있었습니다. 각 방식은 서로의 장단점을 갖고있습니다.
따라서 저희의 상황을 고려해서, 지역적으로 관리하기로 결정했습니다. 그 이유는 UX 개선을 위해 앱 전체에 대한 에러 화면, 앱 일부에 대한 에러 화면을 구분해서 렌더링하는게 필요했기 때문입니다. 또 코드 품질 저하로 인한 단점이 크지 않았기 때문에 이 방식을 선택했습니다.
function IssueList() {
// ...
const [error, setError] = useState('');
const tryToFetchData = async (func: () => void) => {
try {
await func();
} catch (error) {
if (error instanceof AxiosError) {
setError(error.response?.data.message ?? 'Sorry, Unknown Error');
} else {
setError('Sorry, Unknown error');
}
}
};
useEffect(() => {
tryToFetchData(fetchIssueCount);
tryToFetchData(fetchIssues);
}, []);
if (error) {
return (
<>
<ErrorComp message={error} />
</>
);
}
// ...
}
fetchIssue
함수과 useEffect
내부에 존재하고, 따라서 렌더링 후에 추가적인 fetch와 로딩이 이루어지는 것이 원인이었습니다(이전 데이터 렌더 => useEffect내의 fetchIssue 동작 => 새로운 데이터 렌더)const { issue: data, fetchIssue, isLoading } = useIssue();
useEffect(() => {
(async () => {
try {
window.scrollTo(0, 0);
await fetchIssue(Number(params?.id));
} catch (error) {
if (error instanceof AxiosError) {
setError(error.response?.data.message ?? 'Sorry, Unknown Error');
} else {
setError('Sorry, Unknown error');
}
}
})();
}, []);
const [error, setError] = useState('');
if (error) {
return <ErrorComp message={error} />;
}
if (data?.number !== Number(params.id))
return <CenterLoadContainer>{isLoading && <LoadSpinner />}</CenterLoadContainer>;
const PER_PAGE = 10;
const NEXT_PAGE = Math.floor(issueList.length / PER_PAGE) + 1;
if (count < 10) {
setIsEnd(true);
setIsLoading(false);
return;
}
const NEXT_PAGE = Math.floor(issueList.length / PER_PAGE) + 1;
repo/facebook/react
api에서 총이슈 개수를 가지고와서 총개수가 10개미만일경우 실행하지않게 변경했습니다const [isEnd, setIsEnd] = useState(false);
const res = await getIssueList(pathParam, { ...queryParam, page: NEXT_PAGE });
if (res.length === 0) {
setIsEnd(true);
setIsLoading(false);
return;
}