원티드 수업을 통해서 액션, 데이터, 계산을 나누어 컴포넌트 및, 함수를 추상화하는 방법에 대해 배웠습니다.
저 역시 사라 마라 웹 서비스 프런트엔드 개발을 하며 고치긴 해야 했는데, 그 방법을 찾지 못해 고민이었던 코드가 있었는데, 이번 기회에 일단 액션, 데이터, 계산을 담당하는 코드를 분리하는 작업부터 리팩토링을 해보았습니다.
코드 설명
위 코드는 챗 GPT한테 할 질문을 이용자에게 받고 대답받는 로직을 수행하는 모듈화 한 훅입니다. 하지만, 그 과정에서 너무 많은 역할을 하고 있습니다.
- 사용자에게 받은 데이터를 서버에 전송합니다(initial 단계)
- 전송후, questionId를 받으면, ReactQuery를 활용하여 응답이 완성될 때까지 확인하는 데이터요청을 보냅니다. (process 단계)
- 응답을 보낼 때마다 몇 번 보냈는지 확인하며, 이것이 총횟수에 몇 퍼센트인지 계산합니다.(process 단계)
- 서버에서 응답이 완성됐다는 확인을 받으면 answerId를 바탕으로 gpt응답을 가져옵니다(finish 단계)
- 만약 제한 횟수안에 answerId를 받지 못했다면 error단계로 이동합니다.
이 외에도, 훅안의 상태를 초기화하거나, error상태에서 다시 process단계로 갈 수 있도록 조절하는 함수가 있는 모듈입니다. stage라는 데이터를 관리하기 위해 모인 함수들이 있어서 코드 가독성이 떨어지는 훅입니다.
위 동영상처럼 이용자가 질문을 하고 실패, 또는 성공할 때까지 이르는 모든 API통신을 담당하는 코드입니다. 코드 전체를 복붙 하기에는 양이 너무 많아 일부만 가지고 왔습니다.(전체코드)
const useQuestion = (type) => {
// initial, process, finish, error(추가 예정)
const [stage, setStage] = useState('initial');
...
const [progress, setProgress] = useState(0);
const maxCount = 15;
const [maxRequestCount, setMaxRequestCount] = useState(maxCount);
// 질문 재시도 횟수
const retryCount = 10;
// questionID를 바꾼 후에, Question 응답을 받는 코드
const isMouted = useRef(false);
useEffect(() => {
if (isMouted.current) {
if (quesionId) {
setRequestQuestion(true);
} else {
setRequestQuestion(false);
setAnswerId('');
}
} else {
isMouted.current = true;
}
}, [quesionId]);
// 요청 횟수가 증가할때 마다 progress변경
useEffect(() => {
if (isMouted.current) {
setProgress(Math.max(100 - Math.floor((requestCount / maxRequestCount) * 100), 0));
} else {
isMouted.current = true;
}
}, [requestCount]);
// 질문 재시도시 남은 횟수 변경하는 함수
const setRetryRequest = () => {
setMaxRequestCount(retryCount);
setRequestCount(0);
setRequestQuestion(true);
setStage('process');
};
...
export default useQuestion;
리팩토링 과정
- 데이터 / 계산 / 액션 표시하기
// 데이터 계산, 액션을 표시하는 과정과 함께 명시적으로 데이터를 확인 할 수 있도록 했습니다.
// before 주석을 통해 stage가 가질 수 있는 단계를 표현했습니다
// initial, process, finish, error
const [stage, setStage] = useState('initial');
// after StageState라는 객체를 활용하여 표현했으며, 실수로 인한 사이드이펙트 가능성을 줄였습니다
const StageState = {
INITIAL: 'initial',
PROCESS: 'process',
FINISH: 'finish',
ERROR: 'error',
};
const useQuestion(type) => {
const [stage, setStage] = useState(StageState.INITIAL)
}
...
// 추가로 각 함수가 액션인지 계산인지 주석을 통해 표시했습니다
// 요청 횟수가 증가할때 마다 Progess를 증가시키는 코드(액션)
useEffect(() => {
if (isMouted.current) {
// maxRequestCount와 requestCount를 바탕으로 현재 남은 시간(퍼센테이지)를 계산하는 코드(계산)
setProgress(Math.max(100 - Math.floor((requestCount / maxRequestCount) * 100), 0));
} else {
isMouted.current = true;
}
}, [requestCount]);
- 계산 로직에 해당하는 컴포넌트를 바깥으로 분리하기 및 암묵적 의존성 제거하기
// before 계산로직을 액션 함수 내부에 작성하여 코드가독성을 떨어뜨리고 있습니다.
setProgress(Math.max(100 - Math.floor((requestCount / maxRequestCount) * 100), 0));
// after computePorgress라는 함수를 외부에 정의하였으며, 필요한 데이터를 인자로 받아 암묵적 의존성을 제거했습니다.
const computeProgress = (nowProgress, maxProgress) => {
return Math.max(100 - Math.floor((nowProgress / maxProgress) * 100), 0)
}
...
setProgress(computeProgress(requestCount, maxRequestCount));
리팩토링 결과 및 느낀 점
// stage 상태
const StageState = {
INITIAL: 'initial',
PROCESS: 'process',
FINISH: 'finish',
ERROR: 'error',
};
// 최대 요청 시도 횟수
const MaxRequestCount = 15;
// 재시도 시 질문 최대 재시도 횟수
const MaxRequestCountOfRetry = 10;
// 남은 시간(퍼센트)를 개산하는 함수
const computeProgress = (nowProgress, maxProgress) => Math.max(100 - Math.floor((nowProgress / maxProgress) * 100), 0);
const useQuestion = (type) => {
const [stage, setStage] = useState(StageState.INITIAL);
...
const [maxRequestCount, setMaxRequestCount] = useState(MaxRequestCount);
// questionID를 바뀌면, Question 응답을 True로 바꾸어 React Query를 작동시키는 코드(액션)
const isMouted = useRef(false);
useEffect(() => {
if (isMouted.current) {
if (quesionId) {
setRequestQuestion(true);
} else {
setRequestQuestion(false);
setAnswerId('');
}
} else {
isMouted.current = true;
}
}, [quesionId]);
// 요청 횟수가 증가할때 마다 Progess를 증가시키는 코드(액션)
useEffect(() => {
if (isMouted.current) {
setProgress(computeProgress(requestCount, maxRequestCount));
} else {
isMouted.current = true;
}
}, [requestCount]);
// 질문 재시도시 남은 횟수 변경하는 함수(액션)
const setRetryRequest = () => {
setMaxRequestCount(MaxRequestCountOfRetry);
setRequestCount(0);
setRequestQuestion(true);
setStage(StageState.PROCESS);
};
...
}
제가 작업하고 있는 코드는 뷰를 담당하는 리액트 컴포넌트와 한번 기능이 구분된 상태이며, 동일한 상태(데이터)를 다루는 액션들로 이루어지는 훅이었기 때문에, 계산 부분을 리팩터링 하면서 수정할 작업이 많이 없었습니다.
하지만, 데이터와 액션, 계산을 나누면서 그러한 구분을 확실히 하기 위해 코드의 경계를 나누는 것 부터 코드의 가독성이 증가하는 효과를 볼 수 있었습니다. 특히, 서비스를 제공하면서 자주 바뀔 수 있는 MaxRequestCount와 MaxRetryRequestCount를 분리한 것 만으로 다른 팀원의 유지보수를 도와줄 수 있을 거라 생각합니다.
추가적으로, 데이터와 액션을 명시하는 과정에서 다음 리팩토링에 대한 힌트를 얻을 수 있습니다. 현재는 저의 훅에 너무많은 데이터와 그 데이터를 다루는 액션들로 이루어져 있습니다. 이러한 상태에서 데이터를 기준으로 훅을 분리한다면 훅을 조금 더 세분화하며 책임을 나누고 가독성까지 증가시킬 수 있을 것 같다는 아이디어를 얻었습니다.
하나의 모듈을 데이터 / 액션 / 계산으로 나누는 것으로 모듈이나 컴포넌트가 물론 기적적으로 좋아지지는 않았습니다. 하지만, 역할을 나누니 코드가 보이기 시작했고, 더 나은 코드를 위한 개선점까지 찾을 수 있었습니다. 다음 리팩토링 과정에서는 이번에 생각한 아이디어를 바탕으로 useQuestion훅의 책임을 좀 더 나누어 보겠습니다.
'개발' 카테고리의 다른 글
[가상스크롤] 가상스크롤 직접 개발하기 (2) | 2024.10.19 |
---|---|
[SARAMARA] 비즈니스로직 분리하기 (0) | 2023.12.11 |
CORS (0) | 2023.12.06 |
[SaraMara] 아토믹 디자인 도입하기 (2) (0) | 2023.11.29 |
[SaraMara] 아토믹 디자인 패턴 도입하기 (0) | 2023.11.20 |