지난 시간에 이어서 그간 프로젝트 리펙토링을 정리하도록 하겠다.
지난시간에 카카오 지도 api 기능 사용을 위한 script를 동적으로 index.html에 넣는 작업을 진행하였고,
동적으로 받아온 kakao map api 기능 사용을 위해 map.tsx에서 지도가 load 되면 해당 지도를 사용하기 위한 mapApi와 kakao 를 따로 mapContext에 저장하는 작업을 진행했다.
가 나쁘지는 않았지만 나의 목표와 조금 다른 점이 있었다.
어째든 kakao 기능을 전역으로 사용할 수 있도록 하는 것!
mapContext 안에 kakao sdk 를 따로 저장하고, 사용되는 모든 곳에서 이를 불러와서 사용하는 구조에서는 불러온 kakao 가 존재할 때만 기능들이 작동하도록 예외처리를 필수로 작업해줘야 한다.
이는... 오히려 코드를 복잡하게 만들었다.
이전처럼 index.html에 script 태그로 직접 kakao sdk src를 넣는 방식에서는 전역 개체인 window 객체에서 바로 kakao를 구조분해해서 사용할 수 있었다.
그리고 지금도 load가 되면 전역으로 kakao를 사용할 수 있다.
이를 해결한 부분과 기타 redux-toolkit의 slice 들을 수정한 이야기 등을 정리하겠다.
우선 이전의 mapContext.tsx 코드는 다음과 같다.
여기서 setKakao의 코드는 kakao기능을 가지고와서 mapContext 내에서 편의점 마커에 띄울 overlay Ref를 만들고 useState에 kakao를 저장한다.
이전 mapContext.tsx 내 코드
const [kakaoService, setKakaoService] = useState<typeof kakao | null>(null)
const setKakao = (newkakao : typeof kakao) => {
storeOverlay.current = new newkakao.maps.CustomOverlay({
position: new kakao.maps.LatLng(
DEFAULT_KAKAO_COORD.lat,
DEFAULT_KAKAO_COORD.lng
),
zIndex: 1,
})
infoOverlay.current = new newkakao.maps.CustomOverlay({
position: new kakao.maps.LatLng(
DEFAULT_KAKAO_COORD.lat,
DEFAULT_KAKAO_COORD.lng
),
zIndex: 1,
})
setKakaoService(newkakao)
}
하지만 이렇게 저장하지 않아도 app.tsx에서 구현한 바와 같이
kakao script가 로드되어야만 전체 router들을 사용하도록 했기 때문에 전역으로 kakao를 사용해도 된다.
다만 위 두가지 overlay는 구현을 해야하므로 간단히 위 함수는 setOverlay로 이름을 바꾸고 사용하였다.
// 삭제
// const [kakaoService, setKakaoService] = useState<typeof kakao | null>(null)
const setOverlay = () => {
storeOverlay.current = new kakao.maps.CustomOverlay({
position: new kakao.maps.LatLng(
DEFAULT_KAKAO_COORD.lat,
DEFAULT_KAKAO_COORD.lng
),
zIndex: 1,
})
infoOverlay.current = new kakao.maps.CustomOverlay({
position: new kakao.maps.LatLng(
DEFAULT_KAKAO_COORD.lat,
DEFAULT_KAKAO_COORD.lng
),
zIndex: 1,
})
}
그리고 kakaoService를 사용하는 모든 곳을 kakao로 대체했다.
-> window 객체 내에 있는 객체라 window를 생략하고 사용 가능하다.
단. 위처럼 overlay를 new 키워드로 새로 객체로 선언하는 작업을 함수 외부에서 진행하면 안된다.
지도가 만들어지고 사용해야 position에 maps.LatLng
을 사용할 수 있다.
store 페이지와 kakao util에서 사용하는 new kakao.maps.services.Places()
에서도 비슷한 오류를 볼 수 잇다.
관련된 모든 kakao 메서드는 지도가 구현된 후에 사용이 가능한 것 같다.
이전 kakaoService로 mapContext에서 kakao 객체를 저장해 사용할 때는
map.tsx에서 kakao에 새로 map를 구현하고난 kakao를 사용해서 map과 관련된 추가 기능들을 바로 사용할 수 있었다.
하지만 현재 전역으로 사용하는 kakao는 map 구현전의 kakao객체라 map 과 관련된 기능을 사용하지 못하는 것으로 보인다.
-> new kakao.maps.services.Places()
같은 경우는 매개변수로 map 객체를 넣어주면 된다.
ex)
const kakaoSearch = new kakao.maps.services.Places(mapApi)
//여기서 mapApi는 map.tsx에서 생성된 kakao.maps.Map 객체
toolkit의 slice는 다음의 4가지로 구분해서 구현했다.
다른 부분은 크게 수정할 부분이 없었지만 conv와 sort가 조금 리펙토링이 필요해보였다.
주로 정렬기능을 위해 사용하는 slice이다. 하지만 편의점 필터링을 할 때도 사용하고 사용자가 최근 검색한 lat, lng 정보 및 검색한 키워드도 저장해둔다.
const initialState: ListInfo = {
searchWord: '',
searchedCoord: null,
brandData: [],
keywordData: [],
sortType: 'distance',
}
searchWord 검색의 경우는 페이지를 새로고침하여 정보가 사라지더라도 coord(최근 검색 좌표) 가 남아있으면 굳이 필요없을 것이라 판단해 제거했다.
그리고 slice 안 action은 각 state에 새로운 값을 저장하는 식으로 되어있었다.
단, 필터링을 위한 brand와 keyword data는 초기화를 하는 기능을 가지고 있었기에
resetFilter action 을 추가해서 간편히 초기화할 수 있도록 했다.
dispatch(saveBrand([]))
dispatch(saveKeyword([]))
// 위 두개로 나눠진 초기화를
dispatch(resetFilter()) // 로 변경
이전에 했던 작업에서 이상하게도 sort 를 위한 값을 sort slice에 저장하면서 실제 sort action은 위 conv slice에서 작업을 했다.
밀접한 관련이 있는 값이라 slice를 구분했지만 거의 혼용해서 사용하는 모습이었다.
예)
reviewSort: (state) => {
state.sortedStores.sort((a, b) => b.reviewCount - a.reviewCount)
},
starSort: (state) => {
state.sortedStores.sort((a, b) => b.starCount - a.starCount)
},
// 정렬기능 버튼이 있는 컴포넌트에서 sortType에 따라 해당 액션을 dispatch했다.
위 기능은 굳이 slice 안 action으로 정의할 필요없이 따로 정렬을 위한 util 함수를 만들어 정렬기능 컴포넌트에서 수행되도록 했다. (위 action은 모두 제거)
const ListBox: React.FC<ListBoxProps> = ({ stores }) => {
const sortType = useAppSelector(sortTypeSelect)
const sortedStores = useMemo(
() => storeSortAction(sortType, stores),
[sortType, stores]
)
...
return (
<>
...
{sortedStores.map((store) => (
<List
key={store.id}
...
/>
))}
</>
)
sort 기능은 sortType이 변경되거나 편의점이 바뀔 경우에만 수행하도록 useMemo로 감싸뒀다.
이렇게 하므로써 기존에 편의점을 필터링할 때 뿐만아니라 정렬할 때도 새로운 stores가 되어 마커를 새로 생성해 전체 지도위 마커가 깜빡거리는 현상을 막을 수 있었고 각 slice도 좀 더 기능에 맞게 구분되고 심플해질 수 있었다.
가 있었다.
createAsyncThunk는 slice 내 비동기 작업을 수행하기 위해 사용된다.
본 프로젝트에서는 편의점에 대한 추가정보를 서버에 요청해 덧 씌우는 작업을 위 createAsyncThunk에서 수행하였다.
그런데
createAsyncThunk에서 하는 연산량이 많아 redux devtool을 사용할 수 없게 만들었다.
모든 기능은 잘 작동되지만, devtool을 켜기만하면 앱이 다운된다는 것은 문제가 있다는 것이라 판단했다.
const storeIds = mapData.map((result) => result.id)
const stores = await StoreService.getAllStore(storeIds)
const storeData = stores.map((data) => {
const [matchStore] = mapData.filter(
(store) => store.id === data.storeId
)
const customDistance = calcDistance(
map,
Number(matchStore.y),
Number(matchStore.x)
)
return { ...data, ...matchStore, customDistance }
})
if (storeData[0].distance) {
return storeData.sort((a, b) => Number(a.distance) - Number(b.distance))
} else {
return storeData.sort(
(a, b) => Number(a.customDistance) - Number(b.customDistance)
)
}
문제의 createAsyncThunk 안 코드이다.
문제가 되는 부분은 calcDistance 함수이다.
카카오에서 제공하는 편의점 정보는 검색을 한 기능에 따라 거리값의 존재 여부가 달라진다.
위 코드에서는 거리 값의 존재여부와 상관없이 무조건 임의의 거리값(위도 경도 값을 가지고 나름 수학적으로 계산한)을 추가 계산해서 customDistance
라는 이름으로 무조건 추가했다.
위 작업이 검색된 모든 편의점에 대해서 수행되고 또 위 값의 존재여부에 따라 기본적인 정렬작업도 들어가니
하나의 createAsyncThunk가 하기에는 너무 많은 작업이었다....
const storeIds = mapData.map((result) => result.id)
const stores = await StoreService.getAllStore(storeIds)
const storeData = stores.map((data) => {
const [matchStore] = mapData.filter(
(store) => store.id === data.storeId
)
return { ...data, ...matchStore }
})
return storeData
})
거리값이 없는 경우인 편의점 검색(키워드 검색)을 수행한 경우
해당 함수 callback에서 distance계산을 수행하고 편의점.distance에 저장하였다.
그리고 거리값으로 정렬하는 부분은 제거하여 가볍게 하였다.
createAsyncThunk 뿐만 아니라 각 action에서 수행되는 연산을 최소화하는 것이 좋을 것이라 생각하게 되었다.
카카오의 검색 기능은 앞서 언급한 대로 new kakao.maps.services.Places()
로 새로 객체를 생성후 사용가능하다.
위 기능은 앱의 가장 중요한 기능인데 필자는 커스텀 훅처럼 불러서 사용하고자 했다.
// useSearchStore.tsx
const useSearchStore = () => {
const dispatch = useAppDispatch()
const { deleteMarkers } = useContext(MapContext)
const searchCallBack = useCallback(
(
data: kakao.maps.services.PlacesSearchResult,
map: kakao.maps.Map,
searchType: SearchType
) => {
if (searchType === SearchType.KEYWORD) {
키워드 검색일 때 특정 작업수행
}
// 센터 찾아서 가운데 위치 찾고 마커 표시
const lat = map.getCenter().getLat()
const lng = map.getCenter().getLng()
// 좌표저장 및 해당 데이터로 비동기 작업수행
dispatch(setSearchedCoord({ lat, lng }))
dispatch(fetchAllStores({ mapData: data, map }))
},
[dispatch]
)
const searchStore = useCallback(
(searchType: SearchType, mapApi: kakao.maps.Map, searchTerm?: string) => {
dispatch(saveSearchWord(''))
deleteMarkers()
KakaoService.overlay.setMap(null)
if (searchType === SearchType.KEYWORD && searchTerm) {
// 키워드 서치
KakaoService.placeSearch.keywordSearch(
`${searchTerm} 편의점`,
(data, status) => {
if (status === kakao.maps.services.Status.OK) {
searchCallBack(data, mapApi, searchType)
}
}
)
} else {
// 카테고리 서치
KakaoService.placeSearch.categorySearch(
'CS2',
(data, status) => {
if (status === kakao.maps.services.Status.OK) {
searchCallBack(data, mapApi, searchType)
}
},
// 카테고리 서치 옵션
{
location: mapApi.getCenter(),
sort: kakao.maps.services.SortBy.DISTANCE,
useMapBounds: true,
}
)
}
},
[dispatch, searchCallBack, deleteMarkers]
)
return { searchStore }
커스텀 훅이라고 사용을 했지만 굳이 커스텀 훅으로 사용할 필요가 없는 기능인 것이라 판단하게 되었다.
그리고
export enum SearchType {
KEYWORD = 'KEYWORD',
CATEGORY = 'CATEGORY',
}
이와 같이 SearchType
으로 기능사용을 구분하였는데 사실 엄청 많은 부분이 겹치지도 않고 오히려 함쳐서 함수를 사용하니 가독성이 더 안좋아졌다는 느낌을 받았다.
역시 오랜만에 다시보니 보이는 이 지저분함...
위 기능은 커스텀 훅이 아닌 kakao util 함수로서 사용하였고 dispatch와 같은 작업은 callBack 함수를 받아 사용해서 위 함수를 실행하는 쪽에서 사용하도록 변경하였다.
export const kakaoKeywordSearch = (
mapApi: kakao.maps.Map,
searchTerm: string,
callbackFn: (
mapData: kakao.maps.services.PlacesSearchResult,
lat: number,
lng: number
) => void
) => {
const kakaoPlace = new kakao.maps.services.Places(mapApi)
kakaoPlace.keywordSearch(`${searchTerm} 편의점`, (mapData, status) => {
if (status === kakao.maps.services.Status.OK) {
const bounds = new kakao.maps.LatLngBounds()
const lat = mapApi.getCenter().getLat()
const lng = mapApi.getCenter().getLng()
for (let i = 0; i < mapData.length; i++) {
// 키워드 검색시 수행해야하는 특정 작업 수행
// 앞서 언급한 distance 값도 여기서 주입
}
// callBack 함수를 외부에서 받아와 값만 넘겨줌
callbackFn(mapData, lat, lng)
} else {
alert(
`${searchTerm} 편의점이 존재하지 않습니다. 다른 이름으로 검색해주세요`
)
}
})
}
이렇게하고 결과 값으로 해야하는 추가 작업은 함수를 실행한 쪽에서 수행하도록 했다.
kakaoCategorySearch(mapApi, (mapData, lat, lng) => {
dispatch(setSearchedCoord({ lat, lng }))
dispatch(fetchAllStores({ mapData }))
})
이전 코드에 비해 가독성이 많이 좋아졌고 함수가 가지는 책임을 나눠 디버깅이 더 용이해졌다고 판단된다.
이외에 짜잘한 작업을 많이 진행해서 해당 프로젝트의 리펙토링을 어느정도 마무리를 지었다.
위 프로젝트 마무리를 1월에 지었는데 다시 꺼내보니 이렇게 복잡하고 지저분하게 짠 코드가 있다니라는 생각이 들었다.
그때는 나름 자신감있게 짰는데 말이지... ㅠㅠ
여하튼 좋은 경험이었고, 물론 지금도 아직 개선할 부분이 많다고 본다.
하지만 너무 다 수정하면 오히려 그때 팀원들과 만들었던 정체성이 흐려질까봐 일단 이정도로 마무리하려한다.
언젠가 또 리펙토링을 하거나 기능을 좀 더 확장시킬 수 있기를 바라며 마무리하겠다.
감사합니다. 이런 정보를 나눠주셔서 좋아요.