문법 문제 페이지에 접속할때 initializeClickExampleAndSubmitAndAnswer
액션과 getGrammarById
액션이 2번씩 반복되고 있었습니다. getGrammarById
는 서버에 Grammar 조회를 요청하는데, 이로인해 서버에서 동일한 grammar 조회 쿼리가 2번 발생하고 있었습니다.
원인을 파악하고자 하나의 라우터에서 순서대로 실행되는 GrammarBookPage > GrammarBookContainer > GrammarBook > GrammarContainer 컴포넌트들을 뜯어봤습니다.
자세히보니 GrammarBookPage가 매핑된 라우터의 URL로 직접 접속하거나 새로고침해도 액션들이 한 번씩 더 디스패치되고 있었습니다.
GrammarBookContainer컴포넌트에는 getGrammarBook
, getUserWrongGrammarBook
액션이 있습니다. 이 액션들이 반드시 먼저 디스패치된 후에 GrammarContainer컴포넌트에서 initializeClickExampleAndSubmitAndAnswer
와 getGrammarById
액션이 디스패치되어야합니다. 하지만 이 둘은 먼저 디스패치되고 있었습니다.
...
const GrammarBookContainer = ({ grammarBookName }) => {
const loading = useSelector(selectLoading);
const data = useSelector(selectData);
const error = useSelector(selectError);
const grammarIndex = useSelector((state) => state.grammarQuiz.grammarIndex);
const isClickWrongGrammarSwitch = useSelector(
(state) => state.wrongGrammars.isClickWrongGrammarSwitch
);
const userName = useSelector((state) => state.auth.user.userName);
const dispatch = useDispatch();
useEffect(() => {
if (isClickWrongGrammarSwitch) {
dispatch(
getUserWrongGrammarBook({
userName: userName,
grammarBookName: grammarBookName,
})
);
} else {
dispatch(getGrammarBook(grammarBookName));
}
}, [isClickWrongGrammarSwitch, userName, grammarBookName, dispatch]);
if (loading && !data) return <CircleSpinner />;
if (error) return <NotFoundPage />;
const progressRate = data && data.grammarIds ? (grammarIndex / data.grammarIds.length) * 100 : 0;
if (data && data.grammarIds.length !== 0) {
return (
<>
{data && <GrammarBook grammarBook={data} progressRate={progressRate} />}
</>
);
} else {
return <Alert variant="info">틀린 문제가 없어요.</Alert>;
} };
...
const GrammarContainer = ({ grammarIds }) => {
const currentGrammarIndex = useSelector(
(state) => state.grammarQuiz.grammarIndex
);
const isExampleClicked = useSelector(
(state) => state.grammarQuiz.isExampleClicked
);
const isSubmit = useSelector((state) => state.grammarQuiz.isSubmit);
const isAnswer = useSelector((state) => state.grammarQuiz.isAnswer);
const loading = useSelector((state) => state.grammars.grammar.loading);
const data = useSelector((state) => state.grammars.grammar.data);
const error = useSelector((state) => state.grammars.grammar.error);
const isChoice = useSelector((state) => state.grammars.isChoice);
const dispatch = useDispatch();
const handleNextGrammar = () => {
dispatch(clickSubmitButton());
if (isSubmit) {
if (!isAnswer) {
dispatch(addIncorrectGrammarId(grammarIds[currentGrammarIndex]));
}
dispatch(increaseIndex());
}
};
useEffect(() => {
dispatch(initializeClickExampleAndSubmitAndAnswer());
if (currentGrammarIndex < grammarIds.length) {
dispatch(
getGrammarById({
id: grammarIds[currentGrammarIndex],
isChoice: isChoice,
})
);
}
}, [dispatch, grammarIds, currentGrammarIndex, isChoice]);
if (loading || !data) return <CircleSpinner />;
if (error) return <NotFoundPage />;
return (
<>
{isChoice ? (
<GrammarChoice
currentGrammar={data}
onNextGrammar={handleNextGrammar}
isExampleClicked={isExampleClicked}
isAnswer={isAnswer}
isSubmit={isSubmit}
/>
) : (
<GrammarWrite
currentGrammar={data}
onNextGrammar={handleNextGrammar}
isAnswer={isAnswer}
isSubmit={isSubmit}
/>
)}
</>
);
};
export default GrammarContainer;
코드 구조 상 grammarIds
리스트가 있다면 반드시 getGrammarById
가 디스패치되기 때문에 이를 비워야합니다. 문법 문제 페이지로 접속하려면 GramamrBooksPage를 거쳐도되고, 직접 해당 URL로 갈 수 도 있습니다.
다음과 같이 해결했습니다.
grammarIds
는getGrammarBook
액션이 받아온 값을 저장하는data
에 포함되있기 때문에 이를 초기화하는 액션을 추가했습니다. (data
는reducerUtils
객체의 일부입니다.)getGrammarById
가 디스패치될 수 있는 페이지 접속 경로에 해당 액션들을 디스패치하도록 했습니다.grammarIds
는getUserWrongGrammarBook
액션을 통해서도 받아올 수 있습니다. 때문에 이 액션에 대해서도 같은 조치를 취했습니다.
import { createSlice } from "@reduxjs/toolkit";
import { reducerUtils } from "../../lib/asyncUtils";
const initialState = {
grammarBooks: reducerUtils.initial(),
grammarBook: reducerUtils.initial(),
};
const grammarBooksSlice = createSlice({
name: "grammarBooks",
initialState,
reducers: {
//...생략
initializeGrammarBooksAndGrammarBook: (state) => {
state.grammarBooks = reducerUtils.initial();
state.grammarBook = reducerUtils.initial();
},
initializeGrammarBook: (state) => {
state.grammarBook = reducerUtils.initial();
},
},
});
export const {
//..생략
initializeGrammarBooksAndGrammarBook,
initializeGrammarBook,
} = grammarBooksSlice.actions; export default grammarBooksSlice.reducer;
import { createSlice } from "@reduxjs/toolkit";
import { reducerUtils } from "../../lib/asyncUtils";
const initialState = {
userWrongGrammars: reducerUtils.initial(),
saveWrongGrammars: reducerUtils.initial(),
userWrongGrammarBook: reducerUtils.initial(),
isClickWrongGrammarSwitch: false,
};
const wrongGrammarsSlice = createSlice({
name: "wrongGrammars",
initialState,
reducers: {
//...생략
initializeUserWrongGrammarBook: (state) => {
state.userWrongGrammarBook = reducerUtils.initial();
},
},
});
export const {
//...생략
initializeUserWrongGrammarBook,
} = wrongGrammarsSlice.actions; export default wrongGrammarsSlice.reducer;
위와 같이 reducerUtils.initial()
을 할당해 data
를 초기화하는 액션을 정의하고,
const GrammarBooksContainer = () => {
const loading = useSelector(
(state) => state.grammarBooks.grammarBooks.loading
);
const data = useSelector((state) => state.grammarBooks.grammarBooks.data);
const error = useSelector((state) => state.grammarBooks.grammarBooks.error);
const isLoging = useSelector((state) => state.auth.isLoging);
const dispatch = useDispatch();
const handleWrongGrammarSwitch = () => {
dispatch(clickWrongGrammarSwitch());
};
const handleWriteSwitch = () => {
dispatch(clickWriteSwitch());
};
useEffect(() => {
dispatch(initializeGrammarBooksAndGrammarBook());
dispatch(initializeUserWrongGrammarBook());
dispatch(initializeIndex());
dispatch(clearIncorrectGrammarIds());
dispatch(initializeWrongGrammarSwitch());
dispatch(initializeWriteSwitch());
dispatch(initializeGrammar());
dispatch(getGrammarBooks());
}, [dispatch]);
if (loading && !data) return <CircleSpinner />;
if (error) return <NotFoundPage />;
return (
<>
{data && (
<GrammarBooks
grammarBooks={data}
isLoging={isLoging}
onWrongGrammarSwitch={handleWrongGrammarSwitch}
onWriteSwitch={handleWriteSwitch}
/>
)}
</>
);
};
export default GrammarBooksContainer;
GrammarBooksPage로 들어가면 렌더링되는 GrammarBooksContainer에서 디스패치했습니다.
const GrammarBookContainer = ({ grammarBookName }) => {
const loading = useSelector(selectLoading);
const data = useSelector(selectData);
const error = useSelector(selectError);
const grammarIndex = useSelector((state) => state.grammarQuiz.grammarIndex);
const isClickWrongGrammarSwitch = useSelector(
(state) => state.wrongGrammars.isClickWrongGrammarSwitch
);
const userName = useSelector((state) => state.auth.user.userName);
const dispatch = useDispatch();
useEffect(() => {
dispatch(initializeGrammarBook());
dispatch(initializeUserWrongGrammarBook());
if (isClickWrongGrammarSwitch) {
dispatch(
getUserWrongGrammarBook({
userName: userName,
grammarBookName: grammarBookName,
})
);
} else {
dispatch(getGrammarBook(grammarBookName));
}
}, [isClickWrongGrammarSwitch, userName, grammarBookName, dispatch]);
if (loading && !data) return <CircleSpinner />;
if (error) return <NotFoundPage />;
const progressRate = data && data.grammarIds ? (grammarIndex / data.grammarIds.length) * 100 : 0;
if (data && data.grammarIds.length !== 0) {
return (
<>
{data && <GrammarBook grammarBook={data} progressRate={progressRate} />}
</>
);
} else {
return <Alert variant="info">틀린 문제가 없어요.</Alert>;
}
};
export default GrammarBookContainer;
GrammarBookPage로 직접 접속하는 경우 GrammarBookContainer가 렌더링되므로 여기에서도 디스패치했습니다.
이제 getGrammarBook
> initializeClickExampleAndSubmitAndAnswer
> getGrammarById
순서대로 한 번씩만 디스패치됩니다.
액션을 사용하지 않고 grammarIds
리스트만 비우려고 시도해봤으나 Uncaught TypeError: Cannot assign to read only property~가 발생했습니다.
그래서 grammarIds
를 깊은 복사를 통해 따로 할당하고 다시 시도해봤으나, Uncaught TypeError: Cannot read properties of null (reading ‘grammarIds’)가 발생했습니다.
로직을 조금 더 손보면 액션보다 간단한 방법으로 해결할 수 있으리라 생각합니다.
'React' 카테고리의 다른 글
[트러블 슈팅]redux-persist 비직렬화 액션 값 오류 (0) | 2024.02.24 |
---|---|
React에서 scroll fade in 애니메이션 구현하기 (1) | 2023.12.30 |