๐๐ป ์ ๋ชฉ์ ํด๋ฆญํ๋ฉด ๋ฐฐํฌ๋ ์ฌ์ดํธ๋ฅผ ํ์ธํ์ค ์ ์์ต๋๋ค.
- 2023๋ 7์ 11์ผ ~ 2023๋ 07์ 14์ผ
- GitHub REST API๋ก GitHub Issue ํ์ด์ง ๊ตฌํํ๊ธฐ
- ๋ฌดํ ์คํฌ๋กค์ ์ฌ์ฉํด์ HTTPS ์์ฒญ ํด๋ณด๊ธฐ
- โญ ์ด์ ๋ชฉ๋ก ๋ฐ ์์ธ ํ๋ฉด ๊ธฐ๋ฅ ๊ตฌํ
- โญ Context API๋ฅผ ํ์ฉํ API ์ฐ๋
- โญ ๋ฐ์ดํฐ ์์ฒญ ์ค ๋ก๋ฉ ํ์
- โญ ์๋ฌ ํ๋ฉด ๊ตฌํ
- โญ ์ง์ ๋ ์กฐ๊ฑด(open ์ํ, ์ฝ๋ฉํธ ๋ง์ ์)์ ๋ง๊ฒ ๋ฐ์ดํฐ ์์ฒญ ๋ฐ ํ์
-
์ด์ ๋ชฉ๋ก ํ๋ฉด
- โญ ์ด์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ API ํ์ฉ
- โญ open ์ํ์ ์ด์ ์ค ์ฝ๋ฉํธ๊ฐ ๋ง์ ์์ผ๋ก ์ ๋ ฌ
- โญ ๊ฐ ํ์๋ โ์ด์๋ฒํธ, ์ด์์ ๋ชฉ, ์์ฑ์, ์์ฑ์ผ, ์ฝ๋ฉํธ์โ๋ฅผ ํ์
- โญ ๋ค์ฏ๋ฒ์งธ ์ ๋ง๋ค ๊ด๊ณ ์ด๋ฏธ์ง ์ถ๋ ฅ
- โญ ํ๋ฉด์ ์๋๋ก ์คํฌ๋กค ํ ์ ์ด์ ๋ชฉ๋ก ์ถ๊ฐ ๋ก๋ฉ(์ธํผ๋ํฐ ์คํฌ๋กค)
-
์ด์ ์์ธ ํ๋ฉด
- โญ ์ด์์ ์์ธ ๋ด์ฉ ํ์
- โญ โ์ด์๋ฒํธ, ์ด์์ ๋ชฉ, ์์ฑ์, ์์ฑ์ผ, ์ฝ๋ฉํธ ์, ์์ฑ์ ํ๋กํ ์ด๋ฏธ์ง, ๋ณธ๋ฌธ' ํ์
-
๊ณตํต ํค๋
- โญ ๋ ํ์ด์ง๋ ๊ณตํต ํค๋๋ฅผ ๊ณต์ ํฉ๋๋ค.
- โญ ํค๋์๋ Organization Name / Repository Name์ด ํ์๋ฉ๋๋ค.
-
์ปจ๋ฒค์ ์ ์ง์ ํ์ฌ ์ํค์ ์ ๋ฆฌํด ๋์์ต๋๋ค.
-
๊ตฌํ์ ์ฐ์ ์์๋กํ๊ณ , ํธ๋ฌ๋ธ ์ํ ์ด๋ ๋ฆฌํฉํ ๋ง ํ ๋ถ๋ถ์ด ์๋ค๋ฉด, ์ถ๊ฐ์ ์ผ๋ก ์งํํ ์์ ์ ๋๋ค.
FE. |
- ์ด์ ๋ชฉ๋ก
- ์ด์ ์์ธ
- ์ด์ ๋ชฉ๋ก ์ธํผ๋ํฐ ์คํฌ๋กค
GitHub issue Page |
๐ฆ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
-
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๋ฅผ ํ์ฉํ๋ค.
-
-
์ข์๋ ์ | ์์ฌ์ ๋ ์ |
---|---|
|
|