특정 깃헙 레파지토리(angular-cli)의 이슈 목록과 상세 내용을 확인하는 웹 사이트 구축
프로젝트 기간 : 2022년 10월 26일 ~ 2022년 10월 31일
$ git clone https://github.com/pre-onboarding-frontend-7-team-3/angular-cli-github-issue-crawling.git
$ npm install
REACT_APP_API_URL=YOUR_GITHUB_TOKEN // github 토큰 발급 후 root 경로에 .env 파일 생성
npm start
본 프로젝트는 동료학습에 최적화된 과정을 찾아가며 진행했습니다. VSC Live Code extension을 활용해서 라이브 코드 리뷰를 진행하고 각자 구현한 코드에 대한 피드백 및 리팩토링 후 페어 프로그래밍
방식으로 Best Practice를 채택했습니다.
- VSC Live Share
- 개발 초기에 업무를 최대한 작은 단위로 나눈 뒤 같이 프로그래밍하는 방식의 개발 프로세스를 선택했습니다. 단순 화면 공유 기능을 사용했을 때는 동시에 작업을 하거나 다양한 피드백을 주고 받기 어려웠으나 익스텐션을 사용해서 실시간으로 코드를 작성하고 디버깅할 수 있어서 개발 기간을 단축시켰습니다.
- husky를 이용해 commit전 lint 자동 검사
- Octokit API 기반 비동기 통신
- GitHub REST API를 사용해서 특정 repository에 접근하고자 GitHub CLI, JavaScript, cURL 중 빠른 초기 개발 환경 구축과 간소화된 로직을 근거로
Octokit API
를 채택했습니다. GitHub Actions와도 사용이 가능해 배포 자동화를 고려했을 때 적합하다고 판단했습니다.
// client
import { Octokit } from "octokit";
{
/* ... */
}
const octokit = new Octokit({ auth: process.env.REACT_APP_GITHUB_TOKEN });
{
/* ... */
}
export const octokitApi = async (page) => {
const res = await octokit.request("GET /repos/{owner}/{repo}/issues", {
owner: "angular",
repo: "angular-cli",
state: "open",
sort: "comments",
per_page: 10,
page,
});
return res;
};
{
/* ... */
}
export const octokitDetailApi = async (id) => {
const res = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
owner: "angular",
repo: "angular-cli",
issue_number: id,
});
return res;
};
- Context API / useReducer
- 스크롤 이벤트에 발생하는 비동기 요청마다 상태를 업데이트 시켰습니다. 컴포넌트 단에서 여러 상태를 만들기 보다 컨포넌트 간 상태를 공유하고 비동기 요청 형태에 따라 useContext와 userReducer 훅이 적합하다고 의견을 모았습니다. 더 작은 영역에서 확실한 책임을 지도록 커스텀 reducer와 공용 context prodiver 컴포넌트로 로직을 분리해서 관리했습니다.
// IssueContext
import { createContext, useContext, useReducer } from "react";
import issueReducer from "./useIssueReducer";
{
/* ... */
}
export const dispatchContext = createContext("");
export const issuesContext = createContext("");
{
/* ... */
}
const state = { issue: null, issueList: [] };
{
/* ... */
}
export default function IssuesContextWrapper(props) {
const [issueData, dispatch] = useReducer(issueReducer, state);
{
/* ... */
}
return (
<issuesContext.Provider value={issueData}>
<dispatchContext.Provider value={dispatch}>{props.children}</dispatchContext.Provider>
</issuesContext.Provider>
);
}
{
/* ... */
}
export const useIssueContext = () => {
const issueState = useContext(issuesContext);
if (!issueState) {
throw new Error("Error finding issueContext");
}
return issueState;
};
{
/* ... */
}
export const useDispatchContext = () => {
const dispatch = useContext(dispatchContext);
if (!dispatch) {
throw new Error("Error finding dispatchContext");
}
return dispatch;
};
// useIssueReducer
const issueReducer = (state, action) => {
switch (action.type) {
case "INIT_ISSUELIST":
return { ...state.issue, issueList: [...action.initIssue] };
case "ADD_ISSUELIST":
return { ...state.issue, issueList: [...state.issueList, ...action.initIssue] };
case "INIT_ISSUE":
return { issue: action.initIssue, ...state.issueList };
default:
return state;
}
};
export default issueReducer;
- Intersection Observer API
- 무한 스크롤을 구현하는 방법에 있어, scroll event의 사용은 debounce나 throttle을 통한 호출 제어가 필요하며,
높이 값 조사를 위해 offsetTop을 사용할 때는 layout이 매번 새로 그려져
reflow
가 유발됩니다. Intersection Observer의 사용을 통해 호출 제어의 필요 없이 타겟 요소와 상위 요소 상이의 intersection 변화를 비동기적으로 감지해, 클라이언트의 부담을 덜어준다는 의견 공유가 이루어졌습니다. 구현로직을 간소화하고 훅을 통해 독립적인 함수로 분리했습니다.
// useInfinityScroll
import { useRef } from "react";
{
/* ... */
}
const useInfinityScroll = () => {
const observerRef = useRef();
const beginObserving = (callback) => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
callback();
}
},
{ threshold: 1 },
);
observer.observe(observerRef.current);
};
return [observerRef, beginObserving];
};
{
/* ... */
}
export default useInfinityScroll;
- 반응형 디자인
- UI를 데스크톱과 모바일에서 보았을 때 모두 읽기 편하게 구현하기 위해
media-query
를 사용했습니다. 스타일의 재사용성을 고려해서 커스텀 css를 컴포넌트 간 공유하게 했습니다.
// globalStyle.js
import { css } from "@emotion/react";
export const customBodyStyle = css`
/* */
@media screen and (min-width: 480px) {
width: 28rem;
}
@media screen and (min-width: 767px) {
width: 40rem;
}
@media screen and (min-width: 959px) {
width: 50rem;
}
`;
- react-markdown / syntax highlighter
- 초기에 remark 라이브러리와 remark-html을 혼합하여 사용하는 방식을 채택했습니다.
사용 과정 중 특정 태그(코드 블록)에서 레이아웃이 의도치 않게 상위 블록을 초과하는 경우가 발생하였고,
마크다운 레이아웃 및 html 태그 편집에 용이한 마크다운 라이브러리를 사용하자는 의견을 모았습니다.
react-markdown은 html 삽입 시
dangerouslySetInnerHTML
에 의존하지 않아, XSS 공격에도 안전하다는 의견을 공유했습니다.
// Detail.jsx
import Reactmarkdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
{
/* ... */
}
function Detail() {
{
/* ... */
}
return (
<section>
<Reactmarkdown
children={issue?.body}
skipHtml={false}
parserOptions={{ commonmark: true }}
components={{ code: Component }}
/>
</section>
);
}
const Component = ({ children }) => {
{
/* ... */
}
return (
<SyntaxHighlighter language="javascript" customStyle={customStyle}>
{children}
</SyntaxHighlighter>
);
};
{
/* ... */
}
export default Detail;
- git commit message 컨벤션
커밋명 | 내용 |
---|---|
feat | 파일, 폴더, 새로운 기능 추가 |
fix | 버그 수정 |
docs | 제품 코드 수정 없음 |
style | 코드 형식, 정렬, 주석 등의 변경 |
refactor | 코드 리팩토링 |
test | 테스트 코드 추가 |
chore | 환경설정, 빌드 업무, 패키지 매니저 설정등.. |
hotfix | 치명적이거나 급한 버그 수정 |
- branch 컨벤션
브랜치명 | 내용 |
---|---|
develop | 파일, 폴더, 새로운 기능 추가 |
fix | 버그 수정 |
docs | 제품 코드 수정 없음 |
refactor | 코드 리팩토링 |
hotfix | 치명적이거나 급한 버그 수정 |
📂 src
├── 📂 api // Github Issue Data api 관리
│ └── 📄 client
├── 📂 component // 공통 컴포넌트 관리
│ ├── 📂 common
│ │ ├── 📄 Icon // props로 이름 전달받아 svg를 꺼내 사용할 수 있게 함
│ │ ├── 📂 icons // svg를 함수형으로 분리하여 관리
│ │ ├── 📄 Header
│ │ └── 📄 PageUpButton // 페이지 최상단으로 가게 해주는 버튼
│ ├── 📄 Advertisement // wanted 광고 배너
│ ├── 📄 List // issue list 하나에 대한 컴포넌트
│ ├── 📄 Loading // 무한 스크롤 로딩 스피너
│ │
├── 📂 hooks // 로그인과 회원가입 공통처리, 유효한 사용자 인증 등 custom hook 관리
│ └── 📄 useInfinityScroll
├── 📂 pages // 페이지 관리
│ ├── 📄 Home
│ ├── 📄 Detail
│ └── 📄 Error
├── 📂 shared // global style 적용
│ └── 📄 globalStyle
├── 📂 store // context API 관리
│ └── 📄 IssuesContext
├── 📂 utils // 공통적으로 사용되는 util 함수 관리
│ ├── 📄 cutParams // url parameter 분리
│ ├── 📄 dateConverter // api를 통해 제공받는 timestamp 변경
│ └── 📄 markdownParse // .md확장자 파일 형식 파싱
├── 📄 App
└── 📄 index
조은지 (팀장) |
고영훈 (서기) |
김창희 (팀원) |
박정민 (팀원) |
문지원 (팀원) |
이상민 (팀원) |
이지원 (팀원) |
조수진 (팀원) |
---|---|---|---|---|---|---|---|
Joeunji0119 | YeonghunKO | PiperChang | ono212 | moonkorea00 | dltkdals224 | 365support | suzz-in |