-
Notifications
You must be signed in to change notification settings - Fork 4
트러블슈팅
- Issue 1 : React Query 사용시 데이터 업데이트가 실시간으로 되지 않는 문제
- Issue 2 : 자동 로그인
- Issue 3 : 깃
- Issue 4 : 상태관리 도구로 React Query와 Recoil을 사용한 이유
- Issue 5 : Web Font가 적용 안된 문제
- Issue 6 : React Build 이후에 환경 변수값이 적용되지 않는 문제
- Issue 7 : setTimeout 함수 clearout 하기
📁 주문 데이터를 mutation을 했을 때, 수정한 데이터가 곧바로 변경되지 않는 문제가 있었습니다.
- 사실 수집
- 예제 코드에서는 mutation을 했을 때,
useQueryclient
를 사용해서 데이터를 invalidate를 한다.
- 예제 코드에서는 mutation을 했을 때,
const queryClient = useQueryClient()
const { mutate } = useMutate(dataFetch, {
onSuccess: ()=>{
queryClient.invalidateQueries('key')
}
})
- 원인 파악
-
queryClient.invalidateQueries('key'
입력시 key값이 정확하지 않았을 때 이러한 문제가 발생한다. - mutate이후에 변경된 두개 이상의 query의 변수가 useEffect에서 의존성에 전부 들어가 있지 않았다.
-
- 해결 방법
- invalidate하고자 하는 query의 키 값을 정확하게 입력해줍니다.
- useEffect에 의존성 값을 전부 다 넣습니다.
const { isSuccess:isGetProductSuccess, isRefetching:isGetProductRefetching } = useQuery('getProduct')
const { isSuccess:isOrderSuccess, isRefetching:isOrderRefetching } = useQuery('getOrder')
const { mutate } = useMutate(dataFetch, {
onSuccess:()=>{
queryClient.invalidateQueries('getOrder')
queryClient.invalidateQueries('getProduct')
}
})
useEffect(()=>{
if(isGetProductSuccess && isOrderSuccess && !isGetProductRefetching && isOrderRefetching){
// refetching 성공시 동작시켜야할 코드
}
}, [isGetProductSuccess, isGetProductRefetching, isOrderSuccess, isOrderRefetching])
📁 상품 옵션 수정시에 옵션을 추가를 위해서 input field를 추가할 때, 먼저 옵션을 DB에 추가하고 추가 된 데이터를 form에 반영하려고 했지만 invalidate된 이후 다시 불러온 데이터를 form에서 의도한 대로 반영하지 못했습니다.
-
원인 파악
- useState가 비동기로 동작하여 상태가 곧바로 적용되지 않는 문제와 같이 react hook form에서 변경된 데이터를 곧바로 반영하지 못했습니다.
-
해결 방법
- 옵션 추가를 DB에 먼저 등록하고 refetcing한 데이터를 가져와 form에 적용하는 방법을 포기했습니다.
- 사용자가 상품 수정 버튼을 마지막에 눌렀을 때, 데이터를 서버에 한번에 등록하면서 그 뒤에 query를 invalidate하는 방법으로 문제를 해결하였습니다.
-
마무리
- product의 mutate가 성공한 다음에 기존에 있는 option의 수정을 처리하고 그 다음에 등록되지 않은 option을 mutate해야합니다. 코드가 너무 복잡하게 서로 얽혀있어서 나중에 시간이 지난 다음 코드를 보면 코드를 한참 봐야합니다.
- option mutate 이후
isUpdateProductSuccess
,isAddOptionSuccess
이 두 값이 true일 경우 useEffect에서 상품에 대한 query를 invalidate합니다. - 만약 둘 중에 하나라도 실패하면 사용자가 수정하거나 새로 등록한 옵션값이 정상적으로 출력되지 않는 문제가 발생합니다.
const { mutate: updateProductMutation } = useUpdateProductMutation( graphqlReqeustClient(accessToken), { onSuccess: () => { queryClient.invalidateQueries("getProducts"); } } ); const { isSuccess: isAddOptionSuccess, mutate: addProductOptionMutate } = useAddProductOptionsMutation(graphqlReqeustClient(accessToken)); const { isSuccess: isUpdateProductSuccess, mutate: updateProductOptionsMutate } = useUpdateProductOptionsMutation(graphqlReqeustClient(accessToken)); const selectUpdateItemsSubmitHandler = handleSubmit((data) => { const options = optionValue("options"); const addOptions = options .filter((value) => value.optionId === undefined) .map((item) => ({ productId: selectUpdateProduct.id, name: item.name })); const updateOptions = options.filter( (value) => value.optionId !== undefined ); if (options.length !== 0) { const updateData = { productId: selectUpdateProduct.id, name: data.name, price: Number(data.price), imageUrl: (data.imageUrl as string) || undefined, description: (data.description as string) || undefined }; updateProductMutation( { products: updateData }, { onSuccess: () => { updateProductOptionsMutate({ option: updateOptions }); addProductOptionMutate({ option: addOptions }); } } ); return; } optionSetError("options", { message: "반드시 하나 이상의 옵션이 있어야합니다." }); }); useEffect(() => { if (selectUpdateProduct.options) { const setOptions = selectUpdateProduct.options.map((value) => ({ optionId: value.id, name: value.name })); setOptionValue("options", setOptions); } }, []);useEffect(() => { if (isAddOptionSuccess && isUpdateProductSuccess) { setIsModal(false); setSelectUpdateProduct(updateDefault); queryClient.invalidateQueries("getProducts"); } }, [isAddOptionSuccess, isUpdateProductSuccess]);
📁 회원가입 시 사용자 정보만 입력 후 가입을 진행했는데, 사용자 정보에 사업체 정보를 추가해 등록하는 기능을 추가로 구현하려고 할 때 정보가 등록되지 않는 문제가 있었습니다.
-
사실 수집
- 회원가입 mutation(
useSignupMutation
) 을 진행하면accessToken
을 발행하는데 이를 local storage와 전역으로 관리되는userState
에 Recoil로 저장합니다. - 위에서 받은
accessToken
이 있어야 user 정보를 저장하는query
를 호출할 수 있고, 성공시 user id, name, email 을userState
에 저장합니다. - 두 번째의 user 정보가 있어야 가게 등록 mutation(
useAddStoreMutation
)이 가능합니다. - 유저는 회원가입만 하거나 또는 회원가입/사업체 정보 동시 등록 둘 중 하나를 선택할 수 있어야 하므로 선택 값을 state에 저장합니다.
- 위 기능 구현에 필요한 react-query 사용 방법
- query는 컴포넌트가 mount 되면서 자동으로 호출되나
{enabled: false}
로 시점에 실행할 수 있습니다. -
invalidateQueries
를 사용해 명시적으로 query 가 stale 되는 시점을 정할 수 있습니다. - mutation 을 성공하고, 데이터를 fetching 해주는 것이 필요한 경우 해당 시점에서 관련 query를 invalidate 해줍니다.
- onSuccess 는 mutation 이 성공하고 결과를 전달할 때 실행되며, useMutation option의 추가 콜백에서 첫번째로 실행되고, mutate 호출 후 추가 콜백에서 두 번째로 실행됩니다.
- query는 컴포넌트가 mount 되면서 자동으로 호출되나
useMutation(addTodo, { onSuccess: (data, variables, context) => { // I will fire first } }); mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! } });
- 회원가입 mutation(
-
원인 파악
- 회원 가입만 진행하는 경우와 회원 가입/가게 정보 동시 등록을 나누어 가능하게 하려면 실행되는 조건을 나누고 그에 따라 호출되는 함수 작성이 각각 필요합니다.
- react-query 가 작동하는 방식을 이해한 후 적당한 시점에서 mutation과 query 호출해야 기능이 구현됩니다.
-
해결 방법
-
조건 나누기
-
checkStore
(클라이언트 가게 정보 등록 선택 여부)- 클라이언트의 선택 여부를 확인합니다.
- form 작성 필수 여부를 선택합니다.
- user 쿼리를 호출할것인지 또는 바로 login 화면으로 넘어갈 것인지를 지정합니다.
-
saveStore
(클라이언트의 선택에 따라 가게 등록 mutation 호출 여부)- 등록 mutation을 호출하는 용도로만 사용합니다.
-
-
회원 가입만 진행
- signup mutate - 첫 번째 fire 되는
onSuccess
에서meQuery
refetch 합니다. -
meQuery
- user 정보를 recoil로 저장합니다. - useEffect -
meQuery
에서isSuccess
받아서 admin 페이지로 이동합니다.
Update ⇒
Loading
컴포넌트를 따로 작성해meQuery
호출하고 user 정보를 저장하는 것으로 변경함에따라 이를 적용하여 수정했습니다. 1 에서 refetch 없이 add store mutate 실행하는 조건(checkStore
)을 확인한 후 값이 false 이면 바로Loading
페이지 이동2, 3 제거합니다.
- signup mutate - 첫 번째 fire 되는
-
회원 가입, 가게 정보 등록 동시 진행
- signup mutate - 첫 번째 fire 되는
onSuccess
에서meQuery
refetch + 두 번째 fire 되는onSuccess
에서 add store mutate 실행하는 조건(checkStore
)을 확인한 후saveStore
state를 변경합니다. -
meQuery
- 위와 같습니다 - useEffect - add store mutate 실행하여 가게 정보를 저장합니다.
- add store mutate - onSuccess 에서 login 페이지로 이동합니다.
Update ⇒
- **** signup mutate - 첫 번째 fire 되는
onSuccess
에서checkStore
조건을 확인한 후meQuery
refetch + 두 번째 fire 되는onSuccess
에서 add store mutate 실행하는 조건(checkStore
)을 확인 후saveStore
state를 변경합니다. -
meQuery
- user 정보를 recoil로 저장합니다. - useEffect -
checkStore
,saveStore
및 가게 정보 확인 후 add store mutate 실행합니다. - add store mutate -
'store'
key 가진 쿼리 invalidate 후 페이지 이동합니다.
- signup mutate - 첫 번째 fire 되는
-
-
마무리
컴포넌트 생명주기에 따라 mount, unmount 되는 시점을 파악하고, state가 저장되고 업데이트 되는 시점을 고려해 적절한 곳에서 refetching 을 해주어야 기능이 작동했습니다. 이렇게 manual하게 refetching 해주는 것을 추천하지 않는 글도 있었는데 react-query를 더 공부하고 사용해봐야 합니다.
사용자가 새로고침을 하면 상태관리 도구에 저장된 데이터가 사라집니다. 따라서 로그인 정보가 유실되어 로그인을 다시 해야하는 경우가 발생하였습니다.
- 원인 파악
- 상태 관리 도구에 저장된 데이터는 새로고침을 하면 사라집니다.
- App.tsx에 유저 정보를 다시 불러오는 로직이 없습니다.
- 해결책
- 로컬 스토리지에 유저 정보를 저장하고 Recoil에서 user 정보가 사라졌을 경우 로컬스토리지를 참조하여 사용자 정보를 가져와 로그인 정보를 유지시킵니다.
function App() {
const [user, setUser] = useRecoilState(userState);
const { getUser } = useGetUserInfoFromLocalStorage();
useEffect(() => {
const storageUser = getUser();
if (storageUser === undefined) {
return;
}
if (storageUser && !user.isLogin) {
setUser(storageUser);
}
}, []);
return (
<BrowserRouter>
<Router />
</BrowserRouter>
);
}
- 마무리
- 사실 이 방법에 대해서 해결책으로 제시했지만 민감한 정보들이 유출될 수 있기 때문에 좋은 방법은 아니라고 생각합니다. 왜냐하면 로컬 스토리지는 데이터를 삭제하기 전까지 거의 영구적으로 그 값을 저장하기 때문입니다.
- JWT를 사용하는 경우 토큰을 쿠키에 저장하여 새로고침 시에 서버에게 유저 정보를 요청하는 방법이 있습니다. 개인적으로 JWT를 사용한다면 이 방법으로 해결하는게 더 좋지 않았나 하는 아쉬움이 있습니다.
📁 깃 플로우
브랜칭 전략은 처음에 매우 간단했습니다. 코드를 작성하는 사람의 이름과 작업하는 기능에 대해서 적도록 하였습니다.
$ git checkout -b hyunsu/admin-product
프로젝트 중반에 팀원의 제안으로 깃 플로우를 도입하기로 했습니다. 그러나 과연 팀 프로젝트에서 효과적이었는지는 의문입니다. 아마 제대로 사용하지 못했거나 팀원과 함께 규칙을 정할 때 깃 플로우에 대해서 정확히 무엇인지 이해를 못했을 가능성이 있었습니다.
- 효과적이지 않았다고 생각하는 원인들
- main, dev, feat, fix, chord, document, style로 브랜치를 나눠 PR을 했는데, feat로 대부분 작업을 하였고 feat와 fix가 대부분 혼용되어 사용되었습니다.
- chord, style 브랜치는 조금 애매한 부분이 있는 것 같습니다. 초기 프로젝트 셋팅 과정에서는 필요한 브랜치일지는 모르지만 프로젝트가 진행되면서 설치되는 패키지가 있고 코드 스타일을 중간에 바꾸는 경우는 거의 없었습니다. 또한 fix와 chord가 혼용되어 사용되었습니다.
- 효과적이었다고 생각하는 부분들
- 다른 팀원의 브랜치와 충돌할 일이 없었습니다.
- 해결책을 다같이 모색해야할 경우에 브랜치를 찾아 코드를 함께 보는 것이 매우 간편했습니다.
- rebase를 사용하지 않았기 때문에 깃 분기가 매우 혼란스럽습니다. 하지만 브랜치가 어디에서부터 갈라져 나와 코드가 작성되고 merge 되었는지를 눈으로 보는 편이 더 좋을 것 같다고 판단하여 rebase를 사용하지 않았습니다.
- 해결책
- feat를 할 때 수정되는 컴포넌트나 설치된 패키지가 있다면 그냥 feat에 포함시켰습니다.
- fix는 코드를 전반적으로 수정할 때만 사용했습니다.
- dev는 사용하지 않았습니다. 사실 사용하지 못했다고 보는게 맞는 것 같습니다. 서버에 CI/CD를 하면서 dev 브랜치를 활용했어야 했는데 프론트 CI/CD는 프로젝트 후반에 할 수 있었기 때문입니다.(git action을 이해하고 적용하는데 애를 먹었기 때문에) 그리고 애초에 서버를 설정할 때 프론트는 dev용 포트가 없었습니다.
📁 변경된 폴더 구조에 따라 깃허브 원격 및 로컬 저장소 업데이트시 충돌 발생
- 사실 수집- 필요 없는 중간 폴더를 제거하고 구조를 다시 변경하기로 결정 → 하위 폴더에서 작업하던 것들을 한 단계 상위 폴더로 전부 올림
- 한 팀원이 폴더 구조를 작업하고 공용 메인 저장소(
movie
)에 업데이트 후 다른 팀원이 forked 받은 원격 저장소 및 로컬 저장소 동기화를 진행했습니다. 이 과정중에 각자 진행중인 작업 내용이 있어서 충돌이 발생했습니다.
- 한 팀원이 폴더 구조를 작업하고 공용 메인 저장소(
- 원인 파악
- 폴더 구조 변경에 따라
.gitignore
파일 위치가 변동되고, 따라서node_modules
가 git add에 포함되어 저장소에 변경해야 하는 업데이트 내용이 1000k 넘어가게 되었습니다. - 현재 작업하는 내용을 어디에도 기록하지 않고 동기화를 진행하다 보니
git checkout
이 되지 않고, 따라서 동기화가 불가능했습니다.
- 폴더 구조 변경에 따라
- 해결 방법
- git stash를 사용했습니다.
- 폴더 구조 변경 전에 로컬 저장소 브랜치
feature/orderaction/issue#2
에서 작업 중인 내용을 git stash 처리한 후 원격 저장소로 publish 합니다. - 폴더 구조 변경이 완료된 원격 저장소
movie/main
를 가져옵니다. (git pull movie main
하는 과정에서.gitignore
위치가 바뀌었기 때문에 git add 에 포함된node_modules
를 삭제합니다) - 로컬 브랜치에서 commit & pull를 완료하면 이 시점에서
feature/orderaction/issue#2
는 새롭게 수정된 폴더 정보를 가지게 됩니다. git stash 했기 때문에 로컬에서 작업중이던 정보는 포함하고 있지 않습니다. - 동기화를 마무리하려면 pr을 보내 merge를 해야하는데 해당 브랜치는 아직 작업이 완료되지 않은 가장 ahead 된 브랜치이므로,
feature/orderaction/issue#2
가 아닌origin/main
에서 다시 진행하기로 합니다. - 로컬 브랜치
origin/main
로 이동해movie/main
정보를 가져옵니다. - commit + push + pr + merge 로 각 팀원
main
폴더끼리 동기화를 완료합니다. - 다시
feature/orderaction/issue#2
으로 이동해 git stash pop 으로 작업 중이던 내용을 가져와 마무리를 합니다.
- 폴더 구조 변경 전에 로컬 저장소 브랜치
- git stash를 사용했습니다.
- 마무리
- 폴더 구조가 변경이 되었는데 git stash로 작업 중이던 내용은 어떻게 그대로 들어왔는지는 이해되지 않습니다. 폴더 구조는 기록 안하고 어느 폴더의 어느 파일이 변경되었는지만 기록하는 건지 확인이 필요합니다
상태 관리 도구로 무엇을 사용 할 것인지 고민이 많았습니다.
📌 React Query를 사용한 이유
- 많은 사람들이 사용하기 때문에 문제 발생시 해결을 하기 위한 자료가 많습니다.
- query로 불러온 데이터를 캐싱합니다.
- mutate 함수 호출 이후에 비동기 통신이 성공하면 query로 불러온 값을 invalidate 하여 캐싱된 데이터를 refetching 할 수 있습니다. 서버에 다시 데이터를 요청하는 코드를 작성할 필요가 없다는 이점이 발생합니다.
- isSuccess, isLoading과 같은 API 제공해주기 때문에 서버 상태에 따른 UI를 구현하기 위해 따로 상태를 구현을 하지 않아도 되는 이점이 있습니다.
- graphql 때문에 ApolloClient를 고려했으나 React Query가 graphql을 지원 합니다. 그리고 두번째 이점 때문에 쉽게 포기할 수 없었습니다.
- 해결하지 못한 이슈
- React Query가 컴포넌트 안에 포함되면서 컴포넌트가 필요 이상으로 거대해집니다.
- 점점 React Query와 뒤섞여서 코드 유지보수가 매우 어려워 졌습니다.
📌 Recoil 를 사용한 이유
- Recoil은 가볍습니다. 그리고 전역으로 상태 관리를 할 수 있다는 이점이 있습니다.
- 사용 방법이 useState와 같아 사용이 매우 간편합니다.
- React Query로 데이터를 불러올 시에 프론트 앤드에서 사용하기 편리하게 데이터를 다시 가공해야하는 이슈가 발생하였습니다. 이를 Recoil로 해결하였습니다.
Web Font가 UI에 적용되지 않는 문제가 있었습니다.
- 원인 파악
- body에 font-family와 font-size를 적용하였는데 그렇게 하면 font의 기본 값이 UI에 반영되지 않았습니다.
- 해결 방법
- html에 font-family와 font-size를 작성하였습니다. 또한 input, button과 같은 form관련 태그에는 font-family가 적용이 안되기 때문에 개별적으로 font에 관련된 기본 값을 작성해주었습니다.
build 이후에 local에서 사용하던 환경 변수가 프로젝트에 적용되지 않는 문제가 있었습니다.
- 원인 파악
- build 시점에서 env에 포함된 환경 변수가 주입되기 때문에 env에 포함된 secret을 build 이후에 인식하지 못합니다.
- 해결 방법이라고 생각했던 것들
- gitaction에서 build이후 S3로 보내기 전에 env를 주입하는 코드를 작성했습니다. 하지만 이 방법은 secret이 브라우저에서 전부 노출된다는 치명적인 문제가 있었습니다. build된 이후 브라우저 개발자 도구에서 static을 살펴보면 secret을 전부 볼수 있습니다.
- [Create React App 공식 문서](https://create-react-app.dev/docs/adding-custom-environment-variables/)에서는 secret을 절대 env에 포함시키지 말라고 당부합니다.
- 마무리
- secret이 필요한 기능은 반드시 서버를 통해서 해결 해야합니다.
📁 5초 카운트 후 페이지 자동 이동시 setTimeout 함수가 clearout 되지 않는 문제가 있었습니다.
- 사실 수집
- 주문 완료 후 영수증을 출력을 선택하는 화면에서 setTimeout과 setInterval 함수를 사용해 자동으로 화면을 이동하는 모달 구현 중 setTimeout 함수가 clearout 되지 않았습니다.
- 원인 파악
- useEffect 를 사용하지 않고 구현하는 경우, 사용자의 입력 값이 개입하면 setTimeout과 setInterval 함수가 clearout되지 않습니다.
- setState 를 사용해서 상태를 업데이트 할 경우 업데이트 된 상태를 바로 반영되지 않는데, 이는 비동기적으로 작동하기 때문입니다.
- 해결 방법
- 사용자의 개입이 있는 경우와 아닌 경우(자동으로 페이지 이동)의 조건을 나누어서 각각 useEffect 를 실행합니다.
- 모달 내에서 화면이 총 3번 이동하므로 의존성 값에 따라 useEffect 이 실행되고, 카운트는 업데이트 함수 형식으로 작성했습니다.