-
Notifications
You must be signed in to change notification settings - Fork 3
11 21 도원진 개인 멘토링
<멘토링을 진행하며 깨닫거나 새롭게 시도해본 점>?
시간이나 환경적 요인 때문에 코드 전반적인 퀄리티를 올리기 힘들 때는, 나중에 리팩토링 하기 쉽게 선을 그어둔 채로 개발하라는 말씀이 와닿았습니다. 리액트는 컴포넌트 기반의 설계라 그런지 보다 더 책임과 역할을 분리하는 게 쉬웠습니다. 따라서 의존관계가 줄어 후에 리팩토링을 하는데에도 도움이 많이 되었습니다.
또한 소개해주신 ai 코드리뷰도 잘 사용하여 팀원들이 주지 못했던 피드백을 더 채울 수 있어서 좋았습니다.
이전에는 전역스토어에서 구글 맵 컴포넌트가 렌더링 될 때마다 api를 로드해서 지도 라이브러리를 로드해서 지도를 생성했었습니다.
따라서 페이지 전환 시 마다 지도의 로드 속도가 많이 느렸습니다.
//googleMapSlice.ts
initializeMap: async (container: HTMLElement) => {
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new Error('Google Maps API 키가 설정되지 않았습니다.');
}
const loader = new Loader({
apiKey: apiKey,
version: '3.58',
});
try {
await loader.load();
const { Map: GoogleMap } = (await google.maps.importLibrary(
'maps',
)) as google.maps.MapsLibrary;
const markerLibrary = (await google.maps.importLibrary(
'marker',
)) as google.maps.MarkerLibrary;
const map = new GoogleMap(container, INITIAL_MAP_CONFIG);
set({ googleMap: map, markerLibrary });
} catch (error) {
throw new Error('Failed to load Google Maps API');
}
},
너무 비효율적이라서 로더를 따로 빼놓고 싶었는데, 로더를 저장해야할지? 클래스 생성자를 저장하는 방법이 있는지 몰랐습니다.
초기에는 loader
자체를 스토어에 전역상태로 저장해두고, 로더가 이미 있다면, map, place, marker 필요한 라이브러리를 로드하고 지도를 생성하는 식으로 생각했습니다.
하지만 typeof 를 사용하면 생성자도 변수에 저장을 해둘 수 있다는 사실을 알고, 지도, 마커, place에 대한 생성자를 저장한 모듈을 만들었습니다.
//googleMapsAPI-loader.ts
let isApiLoaded = false;
let GoogleMapClass: typeof google.maps.Map | null = null;
let AdvancedMarkerElementClass:
| typeof google.maps.marker.AdvancedMarkerElement
| null = null;
let PlaceClass: typeof google.maps.places.Place | null = null;
이후에 앱 시작시에 이 모듈을 한번 실행하도록 즉시실행함수로 두었습니다.
//index.tsx
(async () => {
await loadGoogleMapsApi();
})();
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
staleTime: 5 * 60 * 1000,
},
},
});
root.render(
<QueryClientProvider client={queryClient}>
<App />
{isDevelopment && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>,
);
이제 구글 지도의 로드 속도가 빨라졌습니다. 체감상 2배정도는 빨라졌습니다. (대단한 최적화는 아니기에 기록은 해두지 않았습니다..)
그런데 이렇게 해두어도 await은 같은 스코프 내의 동기적 코드의 실행만 미룬다는 사실을 알게 되었습니다. 가끔 지도가 렌더링되는 페이지로 바로 접속하면 지도 라이브러리를 인식하지 못했습니다.
따라서 구글 지도 전역 스토어에서 지도를 로드할때 api를 로드하게 두고, loadGoogleMapsApi
가 있는 모듈에 API로드 여부를 저장하는 boolean값으로 즉시 리턴하게 두었습니다.
initializeMap: async (container: HTMLElement) => {
await loadGoogleMapsApi();
const GoogleMap = getGoogleMapClass();
const map = new GoogleMap(container, INITIAL_MAP_CONFIG);
set({ googleMap: map });
},
현재 보고 있는 지도 객체를 전역상태로 관리하기 때문에, 만약에 지도 객체가 있다면 새롭게 지도를 생성하지 않는 캐시 효과도 누릴 수 있을 것 같습니다.
- 남은 과정으로는, 현재 지도에 표시되는 마커가 커스텀되어야 하므로 현재 구글에서 제공하는 마커 props를 커스텀해서 마커 컴포넌트를 재구성해야합니다.
export type MarkerProps = Omit<
google.maps.marker.AdvancedMarkerElementOptions,
'map'
> &
MarkerEventProps;
import { MarkerProps, useMarker } from '@/hooks/useMarker';
const Marker = (props: MarkerProps) => {
const markerInstance = useMarker(props);
return <></>;
};
export default Marker;
- 또한 장소에 한해서, 현재는 한번의 지도, 코스 get Query에서 모든 장소를 가지고 fetch하지만 사용자 지도 뷰영역에 보이는 만큼 query를 날리는 방식으로 고려중입니다.
- 마지막으로 줌아웃이 최대한 되었을 때도, 그 장소에 몇개가 모여있는지 알리는 클러스터링도 구현할 예정입니다.
- 백엔드에서의 최적화가 어렵다면, 프론트단에서만 구글에서 제공하는 마커클러스터 라이브러리를 사용하여 구현해볼 생각입니다. https://developers.google.com/maps/documentation/javascript/marker-clustering?hl=ko
- 왜 토큰을 썼는지?
- 왜 로컬스토리지에 저장했는지?
- 앞의 근거랑 다른 진영에 있는 쿠키 세션 방식을 왜 안했는지? 까지
- 권한 가드 컴포넌트를 래핑을 할지? 아니면 다른 방식을 사용할지?
이제 로그인 기능이 생기면, api 별로 토큰 여부가 달라져야했습니다.
좋은 방법이 없을까 생각하던 중에, axios 타입을 확장하여 useAuth
필드를 만들면 토큰이 필요한 요청과 아닌 요청을 편리하게 관리하는 방법을 알게 되었습니다.
import 'axios';
declare module 'axios' {
export interface AxiosRequestConfig {
useAuth?: boolean;
}
}
export const axiosInstance = axios.create({
baseURL: AXIOS_BASE_URL,
timeout: NETWORK.TIMEOUT,
withCredentials: true,
useAuth: true,
});
현재 로그인 없이도 메인페이지 지도/코스 리스트와 지도 상세보기 페이지는
접근이 가능해야 했습니다. 따라서 해당 api 요청 시에는 useAuth 값을 false로 놓고, 요청 인터셉터를 달아서 옵션에 따라 토큰을 저장할 수 있었습니다. (브라우저의 토큰 관리는 제대로 조사하지 못해서 일단 로컬스토리지에 두었습니다..)
export const checkAndSetToken = (config: InternalAxiosRequestConfig) => {
if (!config.useAuth || !config.headers || config.headers.Authorization)
return config;
const accessToken = localstorage.getStorage(ACCESS_TOKEN_KEY);
if (!accessToken) {
window.location.href = ROUTES.ROOT;
throw new Error('토큰이 유효하지 않습니다');
}
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
};
- React Router 설정 및 오류
<Routes>
내에 <Suspense>
를 직접 감싸서 라우팅 설정을 했더니 다음과 같은 오류가 발생했습니다:
Uncaught Error: [undefined] is not a component. All component children of must be a or <React.Fragment>
- React Router v6에서는
<Routes>
의 직접적인 자식으로<Route>
또는<React.Fragment>
만 허용되었습니다.<Suspense>
를 직접 자식으로 사용하면 이 규칙을 위반하여 오류가 발생합니다. -
<Suspense>
를<Route>
의element
프로퍼티 내에서 사용하거나,<Routes>
전체를<Suspense>
로 감싸는 방식으로 수정해야 했습니다.
지도 생성 시나리오에서 지금까지 추가한 장소 리스트들이 보였으면 좋겠다는 피드백이 있었습니다. 그러기 위해서는 검색을 위한 사이드 탭 옆에 지금까지 추가한 장소 리스트 탭이 있어야 했습니다. 또 장소의 색깔과 코멘트를 입력받는 페이지를 모달로 옮기는 게 좋겠다는 피드백이 있었습니다.
- 지도 애플리케이션에서 왼쪽 대시보드의 장소 아이템을 클릭하면, 대시보드 옆에 동일한 크기의 세부 정보 패널이 열려야 합니다.
- 이때, 모달을 사용할지, 사이드 탭 내에서 상태를 관리할지 고민했습니다.
- 모달을 사용할 경우 ⇒ 사이드 탭의 크기정보를 모달이 알고 있어야 했습니다. 바로 옆에 모달이 열려야 했기 때문.
- 공용 컴포넌트로 이미 구현한 모달이 있었지만, background를 어둡게 하는 등의 위치와 css에서 코드수정이 많아져야 했습니다.
해결
- 보이지 않는 사이드탭의 2배 크기의 Flex 컨테이너를 감싸고, 그 안에 원래 있던 사이드 탭과, 보이지 않는 사이드 탭을 상태로 관리했습니다.
- 사이드 탭의 상태로 두번째 사이드 탭의 렌더링 정보를 boolean으로 관리하게 했습니다.
// SearchPanel.tsx
... 중략
const [isSidePanelOpen, setIsSidePanelOpen] = useState(true);
const location = useLocation();
const mode = location.pathname.split('/')[2].toUpperCase() as CreateMapType;
const {
isOpen: isFormModalOpen,
close: closeFormModal,
open: openFormModal,
} = useOverlay();
return (
<>
<SideContainer>
<BaseWrapper position="" top="" left="" className="w-1/2">
<Box>
<DashBoardHeader title="장소 검색" />
<SearchBar onSearch={(newQuery) => setQuery(newQuery)} />
</Box>
<Box>
<SearchResults query={query} />
</Box>
<AddPlaceButton onClick={openFormModal} />
</BaseWrapper>
{isSidePanelOpen && mapData.places.length && (
<PlaceListPanel
places={mapData.places}
isDeleteMode={true}
isDraggable={mode === 'MAP' ? false : true}
/>
)}
</SideContainer>
...
구글 지도와 관련된 여러 라이브러리를 살펴보며 사용법과 원리를 익히고 있습니다. 리액트와 호환되는 라이브러리 중에 @googlemaps/react-wrapper
와 js-api-loader
를 테스트했는데, @googlemaps/react-wrapper
는 리액트 앱 진입점에서 Google API 로더를 불러와 로딩 처리가 편리하다는 장점이 있지만, render
함수 사용을 강제하고 진입점 컴포넌트 구조를 제한하는 점이 불편하게 느껴졌습니다. 그래서 좀 더 자유롭게 DOM을 직접 제어할 수 있는 js-api-loader
로 전환하여 사용 중입니다. 이 라이브러리는 저수준 라이브러리이므로, 리액트와 DOM 조작을 연결하는 방법에 대해 고민할 기회가 되었습니다. [Google Maps API 라이브러리에 대해](https://www.notion.so/Google-Maps-API-f5e948983b024e5ab2acb84101180509?pvs=21) ( 지속적으로 업데이트 예정입니다.)
현재는 지도와 마커 상태 관리, API 통신 방식에 대해 깊이 탐구하고 있습니다. 지도에서 보이는 부분에 한해 마커를 요청하고, 줌 레벨에 따라 마커 데이터를 다르게 받아오는 방식을 고려 중입니다. 구글 지도를 이용한 전기차 충전소 지도 서비스 사례를 참고하고, 다른 서비스들에서 Axios 설정, API 연결, 커스텀 훅 등의 정보를 참고하고 있습니다.
작업 속도에는 크게 지장을 느끼진 않지만, 방향성에 대해 고민이 많습니다. 다방면에 걸쳐 욕심을 내고 있다는 생각이 들고, 너무 많은 부분을 한꺼번에 다루는 것이 아닌가 걱정되기도 합니다. 특히 백엔드 팀은 기능을 대부분 완성한 반면, 저는 UI 디자인이 아직 확정되지 않아 페이지 구현이 부실한 상태입니다.
UI 디자인이 나오기 전까지는 현재의 속도로 진행하고, 이후에는 한 기능씩 차례대로 구현할 예정입니다.
- 추가로 CodeRabbit 설정을 완료했지만 부스트캠프 측 레포는 접근이 안되어서 개인 레포를 따로 파서 오늘 내로 코드리뷰를 한번 받아볼 생각입니다.
pr에 댓글제한이 걸려서 질문은 못해봤지만, 엄청 세세하게 달아주네요 !!
시 기능은 옵션에서 끌 수가 있더라구요 !
열심히 해보겠습니다 !!