👆🏻 제목을 클릭하면 배포된 사이트를 확인하실 수 있습니다.
- 2023년 7월 11일 ~ 2023년 07월 14일
- 동료학습 및 협업능력과 새로운 인사이트
- GitHub REST API로 GitHub Issue 페이지 구현하기
- 이슈 목록 및 상세 화면 기능 구현
- Context API를 활용한 API 연동
- 데이터 요청 중 로딩 표시
- 에러 화면 구현
- 지정된 조건(open 상태, 코멘트 많은 순)에 맞게 데이터 요청 및 표시
-
이슈 목록 화면
- 이슈 목록 가져오기 API 활용
- open 상태의 이슈 중 코멘트가 많은 순으로 정렬
- 각 행에는 ‘이슈번호, 이슈제목, 작성자, 작성일, 코멘트수’를 표시
- 다섯번째 셀마다 광고 이미지 출력
- 화면을 아래로 스크롤 할 시 이슈 목록 추가 로딩(인피니티 스크롤)
-
이슈 상세 화면
- 이슈의 상세 내용 표시
- ‘이슈번호, 이슈제목, 작성자, 작성일, 코멘트 수, 작성자 프로필 이미지, 본문' 표시
-
공통 헤더
- 두 페이지는 공통 헤더를 공유합니다.
- 헤더에는 Organization Name / Repository Name이 표시됩니다.
- 팀 컨벤션을 지정하여 위키에 정리해 두었습니다.
- 팀 노션을 활용하여 각자의 사전과제를 보고 Best Practice라고 생각되는 과제에 투표를 합니다.
- 팀원들의 원티드 인턴십 사전과제 중, 각각 Best Practice로 선발된 코드를 팀 프로젝트에 반영하고 VScode의 "Live Share" 확장프로그램을 사용하여 서로의 코드 작성법을 실시간으로 보며 함께 리팩토링을 진행하였습니다.
팀장 FE. |
팀원 FE. |
팀원 FE. |
팀원 FE. |
Best Practice란 모범사례라는 말로서, 특정 문제를 효과적으로 해결하기 위한 가장 성공적인 해결책 또는 방법론을 의미합니다.
- 이슈 목록
- 이슈 상세
- 이슈 목록 인피니티 스크롤
GitHub issue Page |
📦src
┣ 📂apis
┃ ┗ 📜issueApi.ts
┣ 📂components
┃ ┣ 📂common
┃ ┃ ┗ 📜Loading.tsx
┃ ┣ 📜Header.tsx
┃ ┣ 📜IssueDetail.tsx
┃ ┣ 📜IssueItem.tsx
┃ ┣ 📜IssueList.tsx
┃ ┗ 📜LocalNavBar.tsx
┣ 📂constants
┃ ┗ 📜color.ts
┣ 📂context
┃ ┗ 📜IssueContext.tsx
┣ 📂httpClient
┃ ┗ 📜httpClient.ts
┣ 📂interface
┃ ┗ 📜issue.ts
┣ 📂lotties
┃ ┗ 📜loading.json
┣ 📂pages
┃ ┣ 📜Issue.tsx
┃ ┗ 📜NotFound.tsx
┣ 📂routes
┃ ┗ 📜router.tsx
┣ 📂utils
┃ ┗ 📜InfiniteScroll.ts
┣ 📜App.tsx
┣ 📜GlobalStyle.ts
┣ 📜index.tsx
┗ 📜react-app-env.d.ts
- 과제의 핵심 기능인 API연동이 깔끔하고 Context API와 HTTP통신의 관심사 분리가 잘 되어 있다고 생각합니다.저번 과제에서 피드백 받았던 부분을 class로 관심사 분리했습니다.
-
Case 01.
-
이슈
- base url 다음에 임의의 값을 넣어주면
/notFound
페이지로 바로 넘어가지 않고, 에러창이 뜹니다. 새로고침해야/notFound
페이지가 렌더됩니다.
const router = createBrowserRouter([ { path: '/', element: <App />, children: [ { path: '/', element: <Issue />, children: [ { path: '/:id', element: <IssueDetail />, errorElement: <Navigate to="/notFound" />, }, ], }, { path: '/notFound', element: <NotFound /> }, { path: '*', element: <Navigate to="/notFound" /> }, ], }, ]);
- base url 다음에 임의의 값을 넣어주면
-
해결
- 비정상적으로 접근을 하면 바로 데이터 요청 없이 바로 "404 Not found"페이지로 이동을 해야하는데 데이터 요청을 하니까 GitHub API에서는
documentation_url: "https://docs.github.com/rest/issues/issues#get-an-issue" message: "Not Found"
에러가 출력됩니다. - 디테일 페이지를 불러올 때, 디테일 페이지를 불러오는 id값 앞에
/issues
경로를 추가합니다. - base url 다음에 임의의 값을 넣어주면 "404 Not found"페이지로 이동하고,
base url/issues/
다음 임의의 값을 넣어주면 '존재하지 않는 이슈'가 화면에 출력됩니다.
const router = createBrowserRouter([ { path: '/', element: <App />, children: [ { path: '/', element: <Issue />, errorElement: <Navigate to="/notFound" />, children: [ { path: '/issues', element: <Navigate to="/" />, }, { path: '/issues/:id', element: <IssueDetail />, }, ], }, { path: '/notFound', element: <NotFound /> }, { path: '*', element: <Navigate to="/notFound" /> }, ], }, ]);
- 비정상적으로 접근을 하면 바로 데이터 요청 없이 바로 "404 Not found"페이지로 이동을 해야하는데 데이터 요청을 하니까 GitHub API에서는
-
-
Case 02.
-
이슈
- 빈값일 때 보여지는 "empty" 이미지와 데이터를 불러오는 중일 때 보여지는 "loading"이 함께 보여집니다. 빈값일 때는 "empty" 이미지만, 데이터를 불러오는 중일 때는 "loading"이 각각 따로 보여졌으면 좋겠습니다.
const IssueDetail = () => { // ... return ( <IssueDetailStyle> {detail ? ( // ... ) : ( <Loading /> )} </IssueDetailStyle> ); };
-
해결
- 삼항연산자를 중복 사용
(조건식(if) ? 참 : 조건식(else if) ? 참 : 거짓(else))
하여detail
데이터가 있으면 렌더링되고, 데이터를 불러오는 중일 땐 "loading"이 보이고, 데이터가 없을 땐 "empty" 이미지를 보여줍니다.
const IssueDetail = () => { // ... return ( <IssueDetailStyle> {detail ? ( // ... ) : isLoading ? ( <Loading /> ) : ( <WarningStyle> <ImWarning /> 존재하지 않는 이슈입니다 </WarningStyle> )} </IssueDetailStyle> ); };
- 삼항연산자를 중복 사용
-
-
Case 01.
-
❌ Bad
- useEffect의 콜백함수는 바로 비동기적일 수 없습니다. 비동기적인 함수를 바로 콜백 함수로 넣을 수 없기 때문에, 즉시실행함수라는 비동기 함수를 useEffect가 실행될 때마다 생성해주고, 그 안에서 데이터를 가져오고 setDetail을 해주게 됩니다. 그리고 만든 즉시실행함수를 실행하게 됩니다. useEffect가 실행될 때마다 매번 함수가 생성되고 실행되니 매우 비효율적입니다.
useEffect(() => { (async () => { try { setDetail(await issueDetail(location)); } catch (error) { console.log(error); } })(); }, [location, issueDetail]);
-
⭕ Good
- useCallback을 활용하면, useCallback안에서 데이터를 가져오는 콜백 함수를 getIssueDetail에 넣어주고, useEffect안에서는 getIssueDetail만 호출을 해주면 코드가 훨씬 깔끔해지고, 로직도 분리되며 가독성도 올라가는 효과를 얻을 수 있습니다.
const getIssueDetail = useCallback(async () => { try { setDetail(await issueDetail(location)); } catch (error) { console.log(error); } }, [issueDetail, location]); useEffect(() => { getIssueDetail(); }, [location, issueDetail, getIssueDetail]);
-
-
Case 02.
-
❌ Bad
- Class 내부에서만 사용하는 상수는 Class 안에서만 사용합니다.
const url = '/repos/facebook/react'; export class IssueApi { private httpClient: HttpClient; constructor(httpClient: HttpClient) { this.httpClient = httpClient; } // ... }
-
⭕ Good
- 클래스는 내부에 데이터를 가지고 있을 수 있기에, 굳이 바깥 범위인 파일에 놔두는 것 보다 내부에서만 사용되는 값이라면 내부로 가져와서 저장해두는 것이 효율적입니다.
- 이 과정에서 외부로 노출될 필요가 없어 private + 수정되지 않는 상수이기에 readonly를 적용합니다.
export class IssueApi { private httpClient: HttpClient; private readonly URL = '/repos/facebook/react'; constructor(httpClient: HttpClient) { this.httpClient = httpClient; } // ... }
-
-
Case 03.
-
❌ Bad
- 유지보수를 위해 CSS-in-JS와 className을 이용한 스타일링 혼용은 금지합니다.
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%); } `;
- 유지보수를 위해 tag selector 사용을 지양합니다.
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}; } } `;
-
⭕ Good
- styled-component를 이용해서 스타일링이 된 태그들은 해당 definition으로 이동해서 바로 스타일링을 확인이 가능한데 className으로 선언한 부분은 바로 definition으로 이동이 불가능해서 스타일링 파악하기에 어려움이 있습니다.
- 위의 내용에 덧붙여서 className을 이용한 스타일링의 nesting depth가 깊어지는 경우에는 더 더욱 찾기가 어려워집니다.
- depth가 깊어지는 경우 뿐만 아니라 1depth로도 className을 이용한 스타일 선언부가 길어지는 경우에도 해당 스타일링을 찾기 어려워집니다.
- 위의 사항들로 인해서 CSS in JS(styled-components 등)을 이용한 스타일링을 하는 경우에는 라이브러리 사용상 강제되는 등의 불가피한 부분을 제외하고는 className을 통한 스타일링은 최대한 지양하는게 권장됩니다.
const IssueUlStyle = styled.ul` width: 100%; padding: 16px; overflow-y: auto; height: calc(100% - 62px); position: relative; `; const EmptyImgStyle = styled.img` width: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); `;
- a, img 태그 처럼 빈번히 사용되는 태그로 스타일을 주는 건 depth에 무관하게 적용되어 의도치 않은 스타일링이 될 수 있습니다.
- tag selector는 해당 요소 뿐만 아니라 동일한 태그로 선언된 다른 요소들에게도 영향을 미칠 여지가 너무 많기 때문에 추후 유지보수에 악영향을 미칠 수 있습니다.
const IssueLiStyle = styled.li` padding: 15px 10px; border-radius: 10px; a { display: flex; justify-content: space-between; align-items: center; } &:hover { background-color: ${COLOR.DarkHover}; h3 { text-decoration: underline; color: ${COLOR.White}; } } `; const ImgStyle = styled.img` background-color: #fff; height: 100%; padding: 0 35%; `;
-
이름 | 좋았던 점 | 아쉬웠던 점 |
---|---|---|
주희 | - 사전 과제보다 성장했습니다. TypeScript 사용, Context API 활용한 API 연동, 라우트 중첩을 프로젝트에 적용할 수 있는 기회였습니다. | - 시간이 너무 촉박했습니다 😭 |
상호 | - Redux와 Context API의 차이점에 대해 에전보다 자세히 알게 되었다. | - 상세 페이지에서 목록 페이지로 돌아왔을 때 데이터가 유지되는 기능을 구현하지 못한 점이 아쉽다. |
민지 |
- 필수 요소가 동일한, 같은 조건 내에서도 사람마다 정말 다르게 구현해 내는 것이 신기했다. - 다른 사람의 코드를 보는 것은 여전히 어렵지만 도움이 많이 됬다. |
- 짧은 기한 내에 구현하려니 좋은 코드를 생각하면서 짤 시간이 부족했다. |
혜림 |
- 글과 화면으로만 배운 class와 Context API를 직접 적용해 볼 수 있어서 좋았다. - 리덕스 툴킷을 잘 알지 못했지만 리덕스 툴킷으로 구현하신 상호님의 코드를 보고 찾아보며 공부할 수 있어서 좋았다. 하지만 그래도 잘 모르겠는게 함정… 🤣 리덕스를 집중적으로 공부하고 프로젝트에 직접 적용해 봐야겠다. |
- 과제 기간이 짧아서 더욱 자세하게 공부하지 못한 것 같아 조금 아쉽다. |