GithubHelp home page GithubHelp logo

hanseungjune / pre-onboarding-github_issue Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 15.1 MB

GitHub_Issue_React

License: Creative Commons Zero v1.0 Universal

Shell 0.58% JavaScript 0.42% HTML 7.39% TypeScript 91.61%

pre-onboarding-github_issue's Introduction

ToyPJT - GitHub Issue Web Page

๐Ÿ‘†๐Ÿป ์ œ๋ชฉ์„ ํด๋ฆญํ•˜๋ฉด ๋ฐฐํฌ๋œ ์‚ฌ์ดํŠธ๋ฅผ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ—“๏ธ ๊ธฐ๊ฐ„

  • 2023๋…„ 7์›” 11์ผ ~ 2023๋…„ 07์›” 14์ผ

๐Ÿงญ ๋ชฉ์ 

  • GitHub REST API๋กœ GitHub Issue ํŽ˜์ด์ง€ ๊ตฌํ˜„ํ•˜๊ธฐ
  • ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•ด์„œ HTTPS ์š”์ฒญ ํ•ด๋ณด๊ธฐ

โœ… Task

โ—ํ•„์ˆ˜ ์š”๊ตฌ ์‚ฌํ•ญ

  • โญ• ์ด์Šˆ ๋ชฉ๋ก ๋ฐ ์ƒ์„ธ ํ™”๋ฉด ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • โญ• Context API๋ฅผ ํ™œ์šฉํ•œ API ์—ฐ๋™
  • โญ• ๋ฐ์ดํ„ฐ ์š”์ฒญ ์ค‘ ๋กœ๋”ฉ ํ‘œ์‹œ
  • โญ• ์—๋Ÿฌ ํ™”๋ฉด ๊ตฌํ˜„
  • โญ• ์ง€์ •๋œ ์กฐ๊ฑด(open ์ƒํƒœ, ์ฝ”๋ฉ˜ํŠธ ๋งŽ์€ ์ˆœ)์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ ์š”์ฒญ ๋ฐ ํ‘œ์‹œ

โ—๋ฒ”์œ„

  1. ์ด์Šˆ ๋ชฉ๋ก ํ™”๋ฉด

    • โญ• ์ด์Šˆ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ API ํ™œ์šฉ
    • โญ• open ์ƒํƒœ์˜ ์ด์Šˆ ์ค‘ ์ฝ”๋ฉ˜ํŠธ๊ฐ€ ๋งŽ์€ ์ˆœ์œผ๋กœ ์ •๋ ฌ
    • โญ• ๊ฐ ํ–‰์—๋Š” โ€˜์ด์Šˆ๋ฒˆํ˜ธ, ์ด์Šˆ์ œ๋ชฉ, ์ž‘์„ฑ์ž, ์ž‘์„ฑ์ผ, ์ฝ”๋ฉ˜ํŠธ์ˆ˜โ€™๋ฅผ ํ‘œ์‹œ
    • โญ• ๋‹ค์„ฏ๋ฒˆ์งธ ์…€๋งˆ๋‹ค ๊ด‘๊ณ  ์ด๋ฏธ์ง€ ์ถœ๋ ฅ
    • โญ• ํ™”๋ฉด์„ ์•„๋ž˜๋กœ ์Šคํฌ๋กค ํ•  ์‹œ ์ด์Šˆ ๋ชฉ๋ก ์ถ”๊ฐ€ ๋กœ๋”ฉ(์ธํ”ผ๋‹ˆํ‹ฐ ์Šคํฌ๋กค)
  2. ์ด์Šˆ ์ƒ์„ธ ํ™”๋ฉด

    • โญ• ์ด์Šˆ์˜ ์ƒ์„ธ ๋‚ด์šฉ ํ‘œ์‹œ
    • โญ• โ€˜์ด์Šˆ๋ฒˆํ˜ธ, ์ด์Šˆ์ œ๋ชฉ, ์ž‘์„ฑ์ž, ์ž‘์„ฑ์ผ, ์ฝ”๋ฉ˜ํŠธ ์ˆ˜, ์ž‘์„ฑ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€, ๋ณธ๋ฌธ' ํ‘œ์‹œ
  3. ๊ณตํ†ต ํ—ค๋”

    • โญ• ๋‘ ํŽ˜์ด์ง€๋Š” ๊ณตํ†ต ํ—ค๋”๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.
    • โญ• ํ—ค๋”์—๋Š” Organization Name / Repository Name์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์ง„ํ–‰๋ฐฉ์‹

  1. ์ปจ๋ฒค์…˜์„ ์ง€์ •ํ•˜์—ฌ ์œ„ํ‚ค์— ์ •๋ฆฌํ•ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

  2. ๊ตฌํ˜„์„ ์šฐ์„ ์ˆœ์œ„๋กœํ•˜๊ณ , ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…์ด๋‚˜ ๋ฆฌํŒฉํ† ๋ง ํ•  ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ์ถ”๊ฐ€์ ์œผ๋กœ ์ง„ํ–‰ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.


๐ŸŽ–๏ธ Members

FE.

๐Ÿ› ๏ธ Stacks

react eslint prettier typescript styledcomponents


๐Ÿ“ ๊ธฐ๋Šฅ

  • ์ด์Šˆ ๋ชฉ๋ก
  • ์ด์Šˆ ์ƒ์„ธ
  • ์ด์Šˆ ๋ชฉ๋ก ์ธํ”ผ๋‹ˆํ‹ฐ ์Šคํฌ๋กค
GitHub issue Page
GitHub issue Page

๐ŸŒณ File Tree

๐Ÿ“ฆsrc
 โ”ฃ ๐Ÿ“‚api
 โ”ƒ โ”ฃ ๐Ÿ“œIssueContext.tsx
 โ”ƒ โ”— ๐Ÿ“œIssueDetailContext.tsx
 โ”ฃ ๐Ÿ“‚components
 โ”ƒ โ”ฃ ๐Ÿ“œErrorScreen.tsx
 โ”ƒ โ”ฃ ๐Ÿ“œHeader.tsx
 โ”ƒ โ”ฃ ๐Ÿ“œIssueContent.tsx
 โ”ƒ โ”ฃ ๐Ÿ“œIssueImg.tsx
 โ”ƒ โ”ฃ ๐Ÿ“œLayout.tsx
 โ”ƒ โ”— ๐Ÿ“œLoading.tsx
 โ”ฃ ๐Ÿ“‚hooks
 โ”ƒ โ”ฃ ๐Ÿ“œuseFetchDetail.ts
 โ”ƒ โ”— ๐Ÿ“œuseFetchIssues.ts
 โ”ฃ ๐Ÿ“‚pages
 โ”ƒ โ”ฃ ๐Ÿ“œDetail.tsx
 โ”ƒ โ”ฃ ๐Ÿ“œIssue.tsx
 โ”ƒ โ”— ๐Ÿ“œNotFound.tsx
 โ”ฃ ๐Ÿ“‚styles
 โ”ƒ โ”— ๐Ÿ“œissuesStyle.ts
 โ”ฃ ๐Ÿ“œApp.tsx
 โ”ฃ ๐Ÿ“œGlobalStyle.ts
 โ”ฃ ๐Ÿ“œindex.tsx
 โ”ฃ ๐Ÿ“œreact-app-env.d.ts
 โ”— ๐Ÿ“œrouter.tsx

โœจ GitHub Issues Web Page

๐Ÿ’ฅ๋ฆฌํŒฉํ† ๋ง

  • Case 01.

    • ๋ผ์šฐํ„ฐ
    <Router>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route
            path="/issue"
            element={
              <IssueProvider>
                <Issue />
              </IssueProvider>
            }
          />
          <Route
            path="/issue/:id"
            element={
              <IssueDetailProvider>
                <Detail />
              </IssueDetailProvider>
            }
          />
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
    • ํ•ด๋‹น์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๋ฉด, App ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ”๋“œ๊ฐ€ ๋ณต์žก์„ฑ์ด ๋„ˆ๋ฌด ๋†’์•„์ง„๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ๊ฐ€ ์•ˆ๋˜์–ด์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ router์—๋Œ€ํ•œ ํŒŒ์ผ์„ ๋”ฐ๋กœ ๋งŒ๋“ค๊ณ  App์— import ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜๋Š”๊ฒŒ ๋งž์ง€ ์•Š๋‚˜ ํ•˜๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด์„œ ๊ทธ๋Ÿฐ ์ƒ๊ฐ์œผ๋กœ ์ž‘์—…ํ–ˆ๋‹ค.
    // ์—ฌ๊ธฐ์„œ๋Š” ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ ˆ์ด์ง€ ๋กœ๋”ฉํ•ฉ๋‹ˆ๋‹ค.
    // ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ์‹œ์ ์—๋งŒ ๋กœ๋“œ๋˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.
    const Issue = lazy(() => import('./pages/Issue'));
    const Detail = lazy(() => import('./pages/Detail'));
    const NotFound = lazy(() => import('./pages/NotFound'));
    
    const RouterComponent = () => (
      <Router>
        <Routes>
          <Route index element={<Issue />} />
          <Route path="/issues" element={<Issue />} />
          <Route path="/issues/:id" element={<Detail />} />
          <Route path="/notFound" element={<NotFound />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </Router>
    );
    
    export default RouterComponent;

    React์˜ lazy๋Š” React ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ”๋“œ ๋ถ„ํ• ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ์›น ํŽ˜์ด์ง€๋Š” ์ฒ˜์Œ ๋กœ๋”ฉ๋  ๋•Œ ๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•œ ๋ฒˆ์— ๋‹ค์šด๋กœ๋“œ๋ฐ›์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๋ฐฉ์‹์€ ํŽ˜์ด์ง€๊ฐ€ ๋ณต์žกํ•ด์ง€๊ณ  ์ปค์งˆ์ˆ˜๋ก ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๊ฐ€ ๋Š๋ ค์ง‘๋‹ˆ๋‹ค.

    lazy๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด๋ ค๊ณ  ํ•  ๋•Œ (์˜ˆ: ํŠน์ • ๋ผ์šฐํŠธ์— ์ ‘๊ทผํ•  ๋•Œ) ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ”๋“œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋„คํŠธ์›Œํฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์ด๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๋ฅผ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

    function App() {
      return (
        <>
          <GlobalStyle />
          <Header />
          <IssueProvider>
            <IssueDetailProvider>
              <Suspense fallback={<Loading />}>
                <RouterComponent />
              </Suspense>
            </IssueDetailProvider>
          </IssueProvider>
        </>
      );
    }
    
    export default App;

    Suspense์™€ fallback ์†์„ฑ์€ React์˜ ์ฝ”๋“œ ๋ถ„ํ•  ๊ธฐ๋Šฅ์„ ๋” ์ž˜ ํ™œ์šฉํ•˜๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

    Suspense๋Š” ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง์ด ์ง€์—ฐ๋˜๋Š” ๋™์•ˆ(์˜ˆ: ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ฑฐ๋‚˜, lazy๋กœ ์ฝ”๋“œ ๋ถ„ํ• ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒฝ์šฐ ๋“ฑ) ์ž„์‹œ๋กœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๋•Œ ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋“ฑ์˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    fallback ์†์„ฑ์€ Suspense ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์„ ๋•Œ ๋ณด์—ฌ์ฃผ๋Š” ๋Œ€์ฒด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, fallback={}๋ผ๊ณ  ์ง€์ •ํ•˜๋ฉด ์•„์ง ๋กœ๋“œ๋˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๋•Œ Loading ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

  • Case 02.

    • Issues && Detail

      • Issues

        const Issue = () => {
          const { issues, loading, error } = useContext(IssueContext);
        
          if (loading) {
            return <Loading />;
          }
          if (error) {
            return <ErrorScreen />;
          }
        
          return (
            <>
              {issues.map((issue, index) => {
                if (issue.state !== 'img') {
                  return (
                    <IssueContent
                      key={issue.id}
                      id={issue.id}
                      title={issue.title}
                      user={issue.user}
                      updateAt={issue.updated_at}
                      comments={issue.comments}
                    />
                  );
                } else {
                  return <IssueImg key={`${issue.src}-${index}`} src={issue.src} />;
                }
              })}
            </>
          );
        };
        
        export default Issue;

        ํ•ด๋‹น ์ฝ”๋“œ๊ฐ€ ์ข€ ์ดํ•ดํ•˜๊ธฐ์—๋Š” ๋ณต์žก์„ฑ์ด ๋†’๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ณ , ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ๊ฐ€ ์ œ๋Œ€๋กœ ๋˜์–ด์žˆ์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ, ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ํ•จ์ˆ˜๋Š” Custom Hook์œผ๋กœ ๋งŒ๋“ค์—ˆ๊ณ , ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐ๊ธˆ ๋” ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ชผ๊ฐค ์ˆ˜ ์žˆ์–ด์„œ, ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•˜๋‚˜ ๋” ๋งŒ๋“ค์—ˆ๋‹ค.

        export const useIssues = () => {
          const { issues, loading, error } = useContext(IssueContext);
        
          return { issues, loading, error };
        };

        Context API์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ฐ€์ง€๊ณ  ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ Hook์œผ๋กœ ๋”ฐ๋กœ ๋นผ๋‚ด์„œ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌ ์‹œ์ผœ๋ฒ„๋ ธ๋‹ค.

        export const renderIssue = (issue: issuesType, index: number) => {
          if (issue.state !== 'img') {
            return (
              <IssueContent
                key={issue.id}
                id={issue.id}
                title={issue.title}
                user={issue.user}
                updateAt={issue.updated_at}
                comments={issue.comments}
              />
            );
          } else {
            return <IssueImg key={`${issue.src}-${index}`} src={issue.src} />;
          }
        };
        
        const Issue = () => {
          const { issues, loading, error } = useContext(IssueContext);
        
          if (loading) {
            return <Loading />;
          }
          if (error) {
            return <ErrorScreen />;
          }
        
          return <>{issues.map(renderIssue)}</>;
        };
        
        export default Issue;

        ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด๋ฒ„๋ฆด๊นŒ ํ•˜๋‹ค๊ฐ€ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋ผ๋Š” ๊ฒƒ์ด ์ƒ๊ฐ๋‚˜์„œ, ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด์„œ UI์ž์ฒด๋ฅผ ๋ถ„ํ• ํ•˜๋˜ ์ ˆ์ฐจ์ง€ํ–ฅ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐฐ์น˜ํ•ด์„œ ์—ฐ๊ฒฐ์„ฑ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•˜์—ฌ ์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ main ์ปดํฌ๋„ŒํŠธ์ธ issues ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์กฐ๊ธˆ ๋” ๊ฐ€๋…์„ฑ์ด ์ข‹์•„๋ณด์˜€๋‹ค.

      • Detail

        const Detail = () => {
          const { id } = useParams();
          const [text, setText] = useState<string[] | null | undefined>([]);
          const { issueDetail, loading, error, fetchIssueDetail } =
            useContext(IssueDetailContext);
        
          useEffect(() => {
            if (id) {
              fetchIssueDetail(id);
            }
          }, [id]);
        
          useEffect(() => {
            const bodySplit = issueDetail?.body?.split('\n');
            setText(bodySplit || []);
          }, [issueDetail.body]);
        
          if (loading) {
            return <Loading />;
          }
          if (error) {
            return <ErrorScreen />;
          }
        
          return (
            <div>
              <IssueContent
                id={issueDetail.number}
                title={issueDetail.title}
                user={issueDetail.login}
                updateAt={issueDetail.updated_at}
                comments={issueDetail.comments}
                img={issueDetail.avatar_url}
              />
              <IssueDetailBodyStyle>
                {text?.map((content: string, idx: number) => {
                  const firstWord = content.split(' ')[0];
                  const remainingContent = content.substring(firstWord.length).trim();
                  let headingLevel = 0;
                  if (
                    firstWord === '#' ||
                    firstWord === '##' ||
                    firstWord === '###' ||
                    firstWord === '####' ||
                    firstWord === '#####' ||
                    firstWord === '#######'
                  ) {
                    headingLevel = firstWord.length;
                  }
                  return (
                    <div
                      key={idx}
                      style={{
                        fontSize:
                          headingLevel !== 0 ? `${24 - headingLevel * 2}px` : 'inherit',
                        fontWeight:
                          headingLevel !== 0 ? 500 + headingLevel * 100 : 'normal',
                      }}
                    >
                      {remainingContent}
                    </div>
                  );
                })}
              </IssueDetailBodyStyle>
            </div>
          );
        };
        export default Detail;

        ํ•ด๋‹น ์ฝ”๋“œ ์—ญ์‹œ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ๊ฐ€ ์ „ํ˜€ ๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋งˆํฌ๋‹ค์šด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ถ€๋ถ„๋“ค์€ ์ปดํŒŒ์ผํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์— ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์•ผํ•  ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

        import { useContext, useEffect } from 'react';
        import { IssueDetailContext } from '../api/IssueDetailContext';
        
        export const useIssueDetail = (id: string | undefined) => {
          const { issueDetail, loading, error, fetchIssueDetail } =
            useContext(IssueDetailContext);
        
          useEffect(() => {
            if (id) {
              fetchIssueDetail(id);
            }
          }, [id]);
        
          return { issueDetail, loading, error };
        };

        Context API์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ๊ฐ€์ง€๊ณ  ์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ Hook์œผ๋กœ ๋”ฐ๋กœ ๋นผ๋‚ด์„œ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌ ์‹œ์ผœ๋ฒ„๋ ธ๋‹ค.

        const Detail = () => {
          const { id } = useParams();
          const { issueDetail, loading, error } = useIssueDetail(id);
        
          if (loading) {
            return <Loading />;
          }
          if (error) {
            return <ErrorScreen />;
          }
        
          return (
            <div>
              <IssueContent
                id={issueDetail.number}
                title={issueDetail.title}
                user={issueDetail.login}
                updateAt={issueDetail.updated_at}
                comments={issueDetail.comments}
                img={issueDetail.avatar_url}
              />
              <IssueDetailBodyStyle className="markdown-body">
                <ReactMarkdown>{issueDetail.body || ''}</ReactMarkdown>
              </IssueDetailBodyStyle>
            </div>
          );
        };
        export default Detail;

        ReactMarkdown ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ†ตํ•ด์„œ issueDetail.body ๋ฅผ ํ†ต์œผ๋กœ ๋ฐ›์•„์™€์„œ ๋งˆํฌ๋‹ค์šด์„ HTML๋กœ ํŒŒ์‹ฑํ•˜๋Š” ์ž‘์—…์„ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  issueDetail์— ๋Œ€ํ•œ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•ด์„œ useIssueDetail๋ผ๋Š” Hook์„ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜์—ฌ export ํ•ด์˜จ๋‹ค.

    • Case 03.

      • Context API (IssueContext)

        ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ์— ๋Œ€ํ•œ ๋ถ„๋ฆฌ๊ฐ€ ๋˜์–ด์žˆ์ง€ ์•Š์•„์„œ Custom Hook๊ณผ ๊ฐ์ข… ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

        // ๊นƒํ—ˆ๋ธŒ์—์„œ ์ด์Šˆ HTTP GET ์š”์ฒญ
        const fetchIssueFromGithub = async (page: number, perPage: number) => {
          const accessToken = process.env.REACT_APP_GITHUB_ACCESS_TOKEN || '';
          const response = await fetch(
            `${process.env.REACT_APP_GITHUB_API_URL}/repos/facebook/react/issues?page=${page}&per_page=${perPage}&sort=comments`,
            {
              method: 'GET',
              headers: {
                Authorization: `Bearer ${accessToken}`,
              },
            }
          );
          const data = await response.json();
          return data;
        };

        ๋จผ์ € HTTPS ์š”์ฒญ์— ๋Œ€ํ•œ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

        // ๋‚ ์งœ๋ฅผ ํ˜•์‹์— ๋งž๊ฒŒ ํฌ๋งทํŒ…
        const formatDate = (dateString: string) => {
          const date = new Date(dateString);
          return `${date.getFullYear()}๋…„ ${date.getMonth() + 1}์›” ${date.getDate()}์ผ`;
        };
        
        // ์ด์Šˆ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„์š”ํ•œ ํ˜•์‹์œผ๋กœ ์ „์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜
        const formatIssuesData = (data: formatIssuesDataType[]) => {
          const items = [];
          for (let i = 0; i < data.length; i++) {
            items.push({
              state: data[i].state,
              id: data[i].number,
              title: data[i].title,
              user: data[i].user.login,
              updated_at: formatDate(data[i].updated_at),
              comments: data[i].comments,
            });
            if (i % 4 === 3) {
              items.push({
                state: 'img',
                src: `https://freight.cargo.site/t/original/i/4578b55ce1658ae2b74841d9148db68944f8461b1d393d29101a372fa80bef12/Logotype_Before_after.jpg`,
              });
            }
          }
          return items;
        };

        ๋ฐ›์•„์˜จ data๋ฅผ ํฌ๋งทํŒ…ํ•˜์—ฌ ์›ํ•˜๋Š” ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

        // ํŽ˜์ด์ง€์™€ ํŽ˜์ด์ง€๋‹น ์ด์Šˆ ์ˆ˜๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„,
        // ์ด์Šˆ๋ฅผ ๊ฐ€์ ธ์™€ ์ƒํƒœ๋ฅผ ์„ค์ •ํ•˜๋Š” Custom Hook
        const useFetchIssues = (perPage: number) => {
          const [issues, setIssues] = useState<issuesType[]>([]);
          const [loading, setLoading] = useState(false);
          const [error, setError] = useState(false);
          const [page, setPage] = useState(1);
        
          // ํŽ˜์ด์ง€, ์ด์Šˆ๋ฅผ ์ƒํƒœ ๊ด€๋ฆฌ
          const fetchIssues = async () => {
            setLoading(true);
            try {
              const response = await fetchIssueFromGithub(page, perPage);
              const formattedData: any[] = formatIssuesData(response);
              setIssues((prev: issuesType[]) => [...prev, ...formattedData]);
              setPage(prevPage => prevPage + 1);
            } catch (error: any) {
              console.log(error);
              setError(error);
            } finally {
              setLoading(false);
            }
          };
        
          return { issues, page, fetchIssues, loading, error };
        };

        ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ”„๋กœ๋ฐ”์ด๋”๋กœ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.

        // ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
        export const IssueContext = createContext({
          issues: [] as issuesType[],
          fetchIssues: () => {},
          loading: true,
          error: false,
        });
        
        // ์ด์Šˆ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์™€ ์ด์Šˆ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ
        export const IssueProvider = ({ children }: any) => {
          const sentinelRef = useRef<HTMLDivElement | null>(null);
          const perPage = 24;
          const { issues, page, fetchIssues, loading, error } = useFetchIssues(perPage);
        
          // Intersection Observer API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์Šคํฌ๋กค์ด ํŽ˜์ด์ง€ ํ•˜๋‹จ์— ๋„๋‹ฌํ–ˆ์„ ๋•Œ ์ƒˆ๋กœ์šด ์ด์Šˆ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
          const handleIntersection = (entries: any) => {
            const target = entries[0];
            if (target.isIntersecting) {
              fetchIssues();
            }
          };
        
          // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋  ๋•Œ ์ด ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๊ต์ฐจ์  ๊ด€์ฐฐ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
          useEffect(() => {
            const observer = new IntersectionObserver(handleIntersection, {
              root: null,
              rootMargin: '0px',
              threshold: 1.0,
            });
        
            // ๊ต์ฐจ์  ๊ด€์ฐฐ ์‹œ์ž‘
            if (sentinelRef.current) {
              observer.observe(sentinelRef.current);
            }
        
            // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋  ๋•Œ ๊ต์ฐจ์  ๊ด€์ฐฐ์„ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค.
            return () => {
              if (sentinelRef.current) {
                observer.unobserve(sentinelRef.current);
              }
            };
          }, [page]);
        
          // ์ด์Šˆ ํ”„๋กœ๋ฐ”์ด๋”๊ฐ€ children ์š”์†Œ์™€ ๊ต์ฐจ์  ์š”์†Œ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.
          return (
            <IssueContext.Provider value={{ issues, fetchIssues, loading, error }}>
              {children}
              <div ref={sentinelRef} style={{ height: '10px' }} />
            </IssueContext.Provider>
          );
        };

        Intersection Observer API ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌดํ•œ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜๋Š” ํ”„๋กœ๋ฐ”์ด๋” ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ•˜๊ณ , ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ๋ฅผ ์œ ์ง€ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

      • Context API (IssueDetailContext)

        ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ์— ๋Œ€ํ•œ ๋ถ„๋ฆฌ๊ฐ€ ๋˜์–ด์žˆ์ง€ ์•Š์•„์„œ Custom Hook์„ ํ†ตํ•ด ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

        export const useFetchDetail = () => {
          const [issueDetail, setIssueDetail] = useState<fetchIssueType>({
            avatar_url: undefined,
            number: null,
            title: null,
            login: null,
            updated_at: null,
            comments: undefined,
            body: null,
          });
        
          const accessToken: string = process.env.REACT_APP_GITHUB_ACCESS_TOKEN || '';
          const [loading, setLoading] = useState(false);
          const [error, setError] = useState(false);
        
          const fetchIssueDetail = async (id: string) => {
            setLoading(true);
            try {
              if ((process.env.REACT_APP_GITHUB_API_URL || '') && id && accessToken) {
                const response = await fetch(
                  `${
                    process.env.REACT_APP_GITHUB_API_URL || ''
                  }/repos/facebook/react/issues/${id}`,
                  {
                    method: 'GET',
                    headers: {
                      Authorization: `Bearer ${accessToken}`,
                    },
                  }
                );
                const data = await response.json();
                let item = {
                  avatar_url: data.user?.avatar_url,
                  number: data.number,
                  title: data.title,
                  login: data.user?.login,
                  updated_at: formatDate(data.updated_at),
                  comments: data.comments,
                  body: data.body,
                };
                setIssueDetail(item);
              }
            } catch (error) {
              console.log(error);
              setError(true);
            } finally {
              setLoading(false);
            }
          };
        
          return { issueDetail, loading, error, fetchIssueDetail };
        };

        useFetchDetail์„ ํ†ตํ•ด์„œ id๋งˆ๋‹ค fetch ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„˜๊ฒจ์ฃผ๊ณ , ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ ํ˜•ํƒœ๋กœ issueDetail์„ ๋งŒ๋“ค์–ด์„œ ๋ณด์—ฌ์ค€๋‹ค.

        // ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
        export const IssueDetailContext = createContext({
          issueDetail: {
            avatar_url: undefined,
            number: null,
            title: null,
            login: null,
            updated_at: null,
            comments: undefined,
            body: null,
          } as fetchIssueType,
          loading: true,
          error: false,
          fetchIssueDetail: (id: string) => {},
        });
        
        // ์ด์Šˆ์˜ ์„ธ๋ถ€ ๋‚ด์šฉ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ
        export const IssueDetailProvider = ({ children }: any) => {
          const { issueDetail, loading, error, fetchIssueDetail } = useFetchDetail();
        
          return (
            <IssueDetailContext.Provider
              value={{ issueDetail, loading, error, fetchIssueDetail }}
            >
              {children}
            </IssueDetailContext.Provider>
          );
        };

        ์ „์—ญ์œผ๋กœ ํ•ด๋‹น ๋ฐ์ดํ„ฐ ๋ฐ fetch ํ•จ์ˆ˜ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด์„œ Context API๋ฅผ ํ™œ์šฉํ•œ๋‹ค.


โœ’๏ธ ํšŒ๊ณ 

์ข‹์•˜๋˜ ์  ์•„์‰ฌ์› ๋˜ ์ 
  • ๋ฌดํ•œ์Šคํฌ๋กค์„ ์˜ˆ์ „ ํ”„๋กœ์ ํŠธ์—์„œ ๊ตฌํ˜„ํ•˜์ง€ ๋ชปํ•ด์„œ ๊ทธ๋ƒฅ Get ์š”์ฒญ์œผ๋กœํ•˜๊ณ , ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ์‚ฌ์šฉํ–ˆ๋˜ ์ ์ด ์žˆ๋Š”๋ฐ, ์ด๋ฒˆ ๊ณผ์ œ๋ฅผ ํ†ตํ•ด์„œ ๋ฌดํ•œ ์Šคํฌ๋กค์— ์ง‘์ค‘ํ•ด๋ณผ ์ˆ˜ ์žˆ์–ด์„œ ์ข‹์•˜์Šต๋‹ˆ๋‹ค.
  • ๋ฆฌํŒฉํ† ๋ง์„ ํ†ตํ•ด์„œ ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ์™€ Custom Hook์— ๋Œ€ํ•ด์„œ ๊นŠ๊ฒŒ ์ƒ๊ฐํ•ด๋ณด๊ณ  ์‹คํ–‰ํ–ˆ๋Š”๋ฐ, ํ•˜๊ณ  ๋ณด๋‹ˆ ๋‚˜์ค‘์— ์œ ์ง€๋ณด์ˆ˜ ์ž์ฒด๋ฅผ ํ•˜๋Š” ๊ฒƒ์— ๋˜๊ฒŒ ํŽธํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค์–ด์„œ ์ข‹์•˜๋˜ ๊ฒƒ ๊ฐ™๋‹ค.
  • ๊ฐœ์ธ์ ์ธ ์‚ฌ์ •์œผ๋กœ ๊ณผ์ œ๋ฅผ 3์ผ ์ •๋„ ๋Šฆ๊ฒŒ ์‹œ์ž‘ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ฑฐ์˜ ํ•˜๋ฃจ์ดํ‹€๋งŒ์— ๋งŒ๋“ค๋ ค๊ณ  ํ•˜๋‹ค๋ณด๋‹ˆ ํ€„๋ฆฌํ‹ฐ๊ฐ€ ์˜ˆ์ƒ๋ณด๋‹ค ์กฐ๊ธˆ ๋–จ์–ด์กŒ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
  • ์›๋ž˜๋Š” ๋™๋ฃŒํ•™์Šต์„ ํ–ˆ์–ด์•ผ ํ–ˆ๋Š”๋ฐ, ๊ฐœ์ธ์ ์ธ ์‚ฌ์ •์œผ๋กœ ํ˜ผ์ž ๊ณผ์ œ๋ฅผ ํ•ด์•ผํ•˜๋Š” ์ž…์žฅ์ด๋ผ์„œ ์กฐ๊ธˆ ์•„์‰ฌ์› ๋‹ค. ๋‹ค์Œ์—๋Š” ๋™๋ฃŒํ•™์Šต์— ์ถฉ์‹คํ•˜์—ฌ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์˜ ์ฝ”๋“œ๋„ ์ฐธ๊ณ ํ•˜๋ฉด์„œ ์„ฑ์žฅํ•˜๊ณ  ์‹ถ๋‹ค.
  • pre-onboarding-github_issue's People

    Contributors

    hanseungjune avatar marie-ming avatar

    Watchers

     avatar

    Recommend Projects

    • React photo React

      A declarative, efficient, and flexible JavaScript library for building user interfaces.

    • Vue.js photo Vue.js

      ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

    • Typescript photo Typescript

      TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

    • TensorFlow photo TensorFlow

      An Open Source Machine Learning Framework for Everyone

    • Django photo Django

      The Web framework for perfectionists with deadlines.

    • D3 photo D3

      Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

    Recommend Topics

    • javascript

      JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

    • web

      Some thing interesting about web. New door for the world.

    • server

      A server is a program made to process requests and deliver data to clients.

    • Machine learning

      Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

    • Game

      Some thing interesting about game, make everyone happy.

    Recommend Org

    • Facebook photo Facebook

      We are working to build community through open source technology. NB: members must have two-factor auth.

    • Microsoft photo Microsoft

      Open source projects and samples from Microsoft.

    • Google photo Google

      Google โค๏ธ Open Source for everyone.

    • D3 photo D3

      Data-Driven Documents codes.