728x90
SMALL
- 드디어 길고 길었던 Conti:ed 서비스 구현이 마무리되고, 배포까지 완료가 되었습니다. 정신없이 구현하고 배포하는 바람에 전혀 하지 못했던 기록들이 밀리고 밀렸습니다.
- 하지만 이대로 그냥 덮어두고 끝내기에는 아쉬웠기 때문에 회고록을 적어보며, 코드 리팩토링도 진행해볼 예정입니다. 제가 주먹구구식으로 구현한 방식이나 구조를 파악하고 조금 더 효율적으로 수정해보고자 합니다.
📢 Conti:ed 개요
- 프로젝트 인원은 2명으로 제가 Frontend 및 AI를 담당하고 다른 인원이 Backend를 담당하여 진행했습니다.
- 간략하게 Conti:ed 서비스에 관해 설명하자면, CCM(기독교 찬양) 관련 재생목록을 사용자가 생성 및 공유하고, 사용자의 입력에 따라 AI 및 유튜브가 재생목록을 생성해주는 서비스라고 보시면 되겠습니다.
- 협업 툴은 주로 Figma, Notion, Github를 사용했고,
- 기술 스택으로는
- Frontend에서 React, Typescript
- Backend에서 Typescript, NestJS, PostgreSQL 등
- AI는 Django 환경 및 OpenAI
- Infra는 Docker, Cloudflare, 클라우드타입을 사용하여 진행하였습니다.
- 개요는 개요일 뿐, 앞으로 코드를 하나하나 뜯어보면서 서비스가 어떤 기능을 제공하고, 어떤 화면을 구상하였으며, 어떤 점이 부족했고, 어떤 문제가 발생했었는지를 기술하며 이와 같은 자세한 사항들은 뒤에 이어질 글들에서 보여드리겠습니다.
- 그럼 먼저 간단히 이 Conti:ed 서비스의 시작점인 index.tsx의 흐름부터 살펴보도록 하겠습니다.
🌊 index.tsx 흐름 요약
- React 서비스가 시작되면 createRoot는 document.getElementById("root")에 UI를 그리게 됩니다.
- RecoilRoot는 상태들을 중앙에서 관리하고 QueryClientProvider는 서버의 데이터 요청과 캐싱을 관리해줍니다.
- RouterProvider는 Router.tsx에 정의된 경로를 사용해 페이지를 이동하도록 설정합니다.
- GlobalSVGProvider를 활용해 SVG 컴포넌트를 더욱 편리하게 사용할 수 있게 되었습니다.
✨ index.tsx 상세 기능
- index.tsx 파일은 React 프로젝트에서 React 서비스를 브라우저 화면에 연결해주는 역할을 하게 됩니다.
- 그 중에서도 저는 몇 가지 라이브러리들을 import하였는데 이에 대해서도 이 글에서는 간략히 설명하고 추후 역할에 대해 기술하도록 하겠습니다.
- 먼저 createRoot부터 설명드리겠습니다.
💡 1. createRoot?
- createRoot는 말 그대로 React의 Root(루트 DOM 노드)를 생성(create)해주는 역할을 합니다. 결국 이 루트를 통해 React가 UI를 그리게 됩니다.
- 기존에는 ReactDOM.render를 사용했었지만, React 18로 넘어오게 되면서 동시성 렌더링을 나타내기 위해 createRoot를 사용하게 되었습니다.
- 또한, 개발할 때 불필요하게 나타나는 사이드 이펙트를 감지하는 역할을 해주는 React.StrictMode와 함께 작동시킬 수도 있어 더욱 간편해졌습니다.
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root")!);
root.render(<서비스 />);
- 저는 위와 같은 방식으로 사용했는데! 이렇게 되면 React가 HTML 문서 안에 있는 <div id="root"></div>에 UI를 그리게 됩니다.
💡 2. RecoilRoot?
- Recoil은 React에서 여러 상태들을 관리하기 위한 라이브러리입니다. 이 Recoil을 사용하기 위해서 React의 Root가 RecoilRoot로 감싸져야 합니다.
- 저는 라이트 / 다크 모드를 구현하기 위해 사용하려고 했다가 우선 라이트 모드 구현부터 해놓고 보자 해서 뒷 순위로 밀려나게 되었습니다.
- 추후 사용되는 곳에서 조금 더 딥하게 언급해보겠습니다.
💡 3. QueryClientProvider?
- react-query의 QueryClientProvider는 서버(백엔드나 AI)와의 데이터 요청이나 캐싱을 관리해주는 역할을 합니다.
- 예를 들자면, API를 통해 POST, PUT 요청을 보낸다든가, 사용자 정보를 가져오는 행위를 도와주는 겁니다.
import { QueryClient, QueryClientProvider } from "react-query";
const 클라이언트 = new QueryClient();
root.render(
<QueryClientProvider client={클라이언트}>
</QueryClientProvider>
);
- 위와 같은 방식으로 QueryClientProvider가 react-query의 전역 클라이언트를 설정해줍니다. 이 클라이언트만 있으면 비동기 로직도 보다 더 쉽게 다룰 수 있습니다.
💡 4. RouterProvider?
- Router의 route는 노선 즉, 경로를 뜻합니다. 자동차가 다니는 경로가 있듯, 서비스에도 페이지 경로가 있습니다.
- 이 경로들을 설정해주는 역할이 바로 Router이며, 추후 나오게 될 Router.tsx 파일에 정의가 되어있는대로 페이지들이 설정됩니다.
- 그래서 이 Router를 설정하기 위해 RouterProvider가 존재하고, 이 친구가 React의 root에게 라우팅 정보를 제공하는 중앙 라우터 역할을 하게 됩니다.
import { RouterProvider } from "react-router-dom";
import 라우팅정보 from "./Router";
root.render(
<RouterProvider router={라우팅정보} />
);
- 위와 같은 방식으로 RouterProvider는 Router.tsx를 참고해서 라우팅 정보를 root에 설정하게 되었습니다.
💡 5. GlobalSVGProvider?
- 이건 사실 제가 SVG를 관리하기 위해 만든 파일입니다.
- 아무래도 사용되는 SVG 파일이 많아지다 보니 파일들을 하나의 폴더에 축적해 보관해놓기가 상당히 불편했고, 한 파일에서도 수많은 SVG 컴포넌트들이 로딩되어져야 해서 그에 따른 라인도 늘게 되었습니다.
- 이런 불편함 때문에 저는 SVG Sprite 기법을 활용해서 큰 파일 내에 여러 개의 아이콘들을 포함시키고, 필요한 부분만 따로 태그를 통해 재사용하게 되었습니다.
- 덕분에 확장성 측면에서도 SVG들을 쉽게 추가할 수 있었고, 페이지의 로딩 시간도 조금은 단축시킬 수 있었습니다.
import { ClassNames } from "@emotion/react";
import { createPortal } from "react-dom";
const spriteSvgCode = (
<ClassNames>
{({ css }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={css`
display: none;
`}
>
<symbol id="back-upload" fill="none" viewBox="0 0 9 16">
<path
d="M8 15L1 8L8 1"
stroke="#545F71"
strokeLinecap="round"
strokeLinejoin="round"
/>
</symbol>
<symbol id="remove-search" fill="none" viewBox="0 0 18 18">
<path
d="M4.5 13.5L13.5 4.5M4.5 4.5L13.5 13.5"
stroke="#545F71"
strokeLinecap="round"
strokeLinejoin="round"
/>
</symbol>
/* 이외 다른 심볼들도 많아요..! */
</svg>
)}
</ClassNames>
);
export default function GlobalSVGProvider() {
return createPortal(spriteSvgCode, document.body);
}
- 이렇게 관리하고 각 심볼의 id만 가져와서 SVG 컴포넌트를 페이지에 보여주게 되면 훨씬 간편하게 되죠!!
🫢 한 눈에 보는 index.tsx
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { RecoilRoot } from "recoil";
import { RouterProvider } from "react-router-dom";
import 라우팅정보 from "./Router";
import GlobalSVGProvider from "./GlobalSVGProvider";
const root = createRoot(document.getElementById("root")!);
// API 상태 관리 설정
const 클라이언트 = new QueryClient();
// React 서비스 렌더링
root.render(
<RecoilRoot>
<QueryClientProvider client={클라이언트}>
<RouterProvider router={라우팅정보} />
<GlobalSVGProvider />
</QueryClientProvider>
</RecoilRoot>
);
- 그런데 사실 이렇게 확인해보면서 root에는 가독성 있게 딱 서비스(App)만 표시되게끔 설정을 해줬어야 했는데, 지금은 여러 Provider가 중첩되어 있어서 가독성이 떨어지는 듯 보입니다.
- 또한, createRoot가 document.getElementById("root")로 HTML 문서 내에 있는 root를 찾게 되는데 잘못 설정되어있거나, 없을 경우에 대한 대비책이 없습니다.
- 따라서 별도의 App 컴포넌트로 Provider들을 분리하고, 에러도 핸들링하기 위한 코드를 추가하여 다음과 같이 리팩토링하였습니다.
🔨 리팩토링된 index.tsx & App.tsx
/* index.tsx */
import { createRoot } from "react-dom/client";
// Provider들을 몽땅 App으로 옮겨버렸습니다
import App from "./App";
const rootElement = document.getElementById("root");
// 추가된 에러 핸들링
if (!rootElement) {
throw new Error(
"root element를 찾을 수가 없어요. HTML 파일 확인 부탁드려요!"
);
}
const root = createRoot(rootElement);
root.render(<App />);
/* App.tsx */
import { RecoilRoot } from "recoil";
import { QueryClient, QueryClientProvider } from "react-query";
import { RouterProvider } from "react-router-dom";
import 라우팅정보 from "./Router";
import GlobalSVGProvider from "./GlobalSVGProvider";
const 클라이언트 = new QueryClient();
const App = () => (
<RecoilRoot>
<QueryClientProvider client={클라이언트}>
<RouterProvider router={라우팅정보} />
<GlobalSVGProvider />
</QueryClientProvider>
</RecoilRoot>
);
export default App;
- 다음은 Router.tsx로 가기 전에 Root.tsx 한 번 살펴보겠습니다.
728x90
LIST
'Projects > Conti:ed' 카테고리의 다른 글
[React] useCallback (0) | 2024.07.10 |
---|---|
[React] useRef (0) | 2024.07.03 |
[React] useEffect (0) | 2024.06.29 |
[React] useState (0) | 2024.06.28 |