퍼널을 구현하면서 각 단계를 하나의 공통 컴포넌트로 합치는 작업 중 문제가 발생했다. 원래 개별적으로 구현되어 있었던 Question1
, Question2
와 같은 단계를 Question.tsx
, AnswerList.tsx
로 통합하고, map
을 통해 렌더링하도록 수정했지만, 렌더링이 아예 되지 않는 상황이 벌어진 것이다.
map
으로 처리한 부분에서 다음과 같은 에러가 발생했다.
funnel 부분 코드 작성에 많은 시간을 들인 터라, 급한 마음에 에러를 해결하려고 타입 단언을 남발하고,(안 좋은거 알지만...) 이미 꼼꼼하게 선언했던 타입을 유연하게 변경하는 등 온갖 시도를 다 해봤다.
하지만 그렇게 정신 없이 뜯어 고치다보니 점점 코드가 복잡해졌고, 문득 이러다가 돌이킬 수 없겠다(?)라는 생각이 들어 코드를 다시 원점으로 돌리고 침착하게 하나씩 콘솔을 찍어가면서 문제 원인을 파악해보고자 했다.
첫 번째 단서
에러 메세지에 StepProps
가 있었기에 StepProps
를 선언했던useFunnel.tsx
파일로 이동했다.
// src/app/recommend/_hooks/useFunnel.tsx
import { useSearchParams } from "next/navigation";
import { useEffect, useState, type ReactElement, type ReactNode } from "react";
export type StepProps = {
name: string;
children: ReactNode;
};
type FunnelProps = {
children: Array<ReactElement<StepProps>>;
};
export const useFunnel = (initialStep: string) => {
const searchParams = useSearchParams();
// 초기 상태 설정
const [currentStep, setCurrentStep] = useState<string>(
() => searchParams.get("step") || initialStep,
);
// 상태와 url 동기화
useEffect(() => {
const stepParam = searchParams.get("step");
if (stepParam) {
setCurrentStep(stepParam);
} else {
window.history.replaceState(null, "", `?step=${currentStep}`);
}
}, [searchParams]);
const Step = ({ name, children }: StepProps): ReactElement => {
return <>{children}</>;
};
const Funnel = ({ children }: FunnelProps) => {
const steps = children.filter((child) => child.type === Step);
const activeStep = steps.find((child) => child.props.name === currentStep);
return activeStep || null;
};
const updateStep = (step: string): void => {
setCurrentStep(step);
window.history.pushState(null, "", `?step=${step}`);
};
const next = (nextStep: string): void => {
updateStep(nextStep);
};
const prev = (prevStep: string): void => {
updateStep(prevStep);
};
return { Funnel, Step, next, prev };
};
두 번째 단서
dev
모드에서 실행했을 때, URL에 ?step=answer1
과 같은 쿼리 파라미터는 정상적으로 반영되고 있었다.
따라서 그 외의 부분이며 StepProps
를 사용하고 있는 아주 수상한 Funnel
부분을 살펴보게 되었다.
const Funnel = ({ children }: FunnelProps) => {
const steps = children.filter((child) => child.type === Step);
const activeStep = steps.find((child) => child.props.name === currentStep);
console.log(currentStep); // 정상 출력: answer1
console.log(activeStep); // undefined 출력
return activeStep || null;
};
콘솔을 찍어보니 currentStep
은 정상적으로 출력되지만, activeStep
이 undefined
로 출력되는 문제를 발견할 수 있었다.
그에 따라 find
메서드가 제대로 동작하지 못하고 있다고 판단해 children
을 출력해보았다.
Children [
[
{ type: [Function: Step], key: 'answer1', ... },
{ type: [Function: Step], key: 'answer2', ... },
{ type: [Function: Step], key: 'answer3', ... },
{ type: [Function: Step], key: 'answer4', ... }
],
{ type: [Function: Step], key: null, props: { name: 'result', ... } }
]
!!!!
평평한 배열로 출력되어야 할children
이 중첩 배열 형태로 출력되었다.
배열의 첫 번째 요소가 중첩 배열로 감싸져 있었기에 메소드가 제대로 동작하지 않았던 것이다.
따라서 다시 map
메소드를 사용한 부분으로 돌아가 children
을 다음과 같이 평탄화해서 중첩 배열을 제거했다.
<Funnel>
{[...questions.map((q, index) => (
<Step key={q.id} name={q.id}>
<Question
question={q.question}
selectedAnswer={answerData[q.id]}
answerItems={q.answers}
onNext={(data) => handleNext(data, steps[index + 1])}
onPrev={(data) => handlePrev(data, steps[index - 1])}
fieldKey={q.id}
/>
</Step>
)),
<Step name="result">
<Result answerData={answerData} />
</Step>]}
</Funnel>
이렇게 배열을 펼쳐주니 바로 문제가 해결됐다.
코드가 의도치 않게 동작하지 않을 때, 급하게 비권장 방식으로 수정하기보단 문제의 근본적인 원인을 찾기 위한 디버깅 과정이 중요하다는 걸 다시 한 번 느낄 수 있었다. 특히 중첩 배열이라는 단순한 원인을 해결하고나니, 처음 시도했던 타입 단언이나 비효율적인 수정 없이도 코드는 정상적으로 동작했다;;
많은 공을 들인 코드든, 급한 상황이든 항상 침착하게 하나씩 디버깅하는 습관을 들여야겠다고 생각했다...