목차
· 배경
· 한 목표를 향하는 퍼널의 문제점
· 퍼널을 풀어내기 위한 시도
배경
회고라는 주제를 다루고 있는 레이어라는 서비스에서 한 화면에서의 여러 페이즈를 통해 화면 흐름을 제어하고, 결국 여러 페이즈를 통해 공통된 목표를 달성하는 퍼널을 만들어야하는 경우가 있었어요. 화면 별로 따로따로 구성을 하기에는 사용자에게 화면의 자연스러움을 제공하고 싶었고, 이에 따라 자연스러운 흐름을 위해 한 화면에서 여러 페이즈(단계)를 관리하고 최종적인 목표를 달성하는 방향으로 퍼널을 관리하고자 했습니다.
한 목표를 향하는 퍼널의 문제점
서비스에서는 여러 핵심 기능이 존재하지만, 그 중 회고 작성 화면에서 하나의 뷰를 담당하는 .tsx 파일에서 여러 과정을 거쳐 최종 목적지인 회고 완료 페이지까지의 여정을 진행해야합니다. 페이지 별로 이를 구현도 가능하지만 각 페이즈에 맞는 화면을 구성할 때마다 상태를 명시해줘야하며, 이에 따라 상태 관리가 어려워지는 문제점을 가지고 있습니다.
퍼널을 풀어내기 위한 시도
처음에는 여러 뷰에 대한 파일을 만들어놓고, 단계에 따라 계속해서 점프 페이지를 하는 순간으로 구현을 진행했어요. 그러다보니 화면 별 상태 관리의 어려움과 화면과 파일이 많아질수록 정말 비효율적인 작업들이 많아지는 것을 확인할 수 있었어요. 그래서 레이어 서비스에서는 이러한 퍼널을 해결하기 위해서 ContextAPI를 통해 부모에서 상태를 관리하고, 자식 요소에서 변경되는 상태를 전역으로 받아 처리를 할 수 있도록 했어요. 그리고 이때 상태 값에 따라 보여지는 화면에 대해서는 JSX의 스위치 문을 통해 상태를 명확하게 구분하고자 했어요, 이를 테면 아래 코드와 같이 createContext를 통해 부모 요소에서 Context를 사전 지정을 해주었어요.
import { createContext, Fragment, useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { LoadingModal } from "@/component/common/Modal/LoadingModal.tsx";
import { Prepare, Write } from "@/component/write/phase";
import { useGetQuestions } from "@/hooks/api/write/useGetQuestions.ts";
import { RetrospectType } from "@/types/write";
export type QuestionData = {
isTemporarySaved: boolean;
questions: {
order: number;
question: string;
questionId: number;
questionType: RetrospectType;
}[];
};
type PhaseContextProps = {
data: QuestionData;
phase: number;
movePhase: (phase: number) => void;
incrementPhase: () => void;
decrementPhase: () => void;
spaceId: number;
retrospectId: number;
};
export const AdvanceQuestionsNum = 2;
export const PhaseContext = createContext<PhaseContextProps>({
data: { isTemporarySaved: false, questions: [] },
phase: 1,
spaceId: -1,
retrospectId: -1,
movePhase: () => {},
incrementPhase: () => {},
decrementPhase: () => {},
});
function adjustOrder(data: QuestionData): QuestionData {
return {
...data,
questions: data.questions.map((q) => ({
...q,
order: q.order - 1,
})),
};
}
export function RetrospectWritePage() {
const location = useLocation();
const { spaceId, retrospectId } = location.state as { spaceId: number; retrospectId: number };
const [phase, setPhase] = useState(-1);
const { data, isLoading } = useGetQuestions({ spaceId: spaceId, retrospectId: retrospectId });
const defaultData: QuestionData = { isTemporarySaved: false, questions: [] };
const [adjustedData, setAdjustedData] = useState<QuestionData>();
useEffect(() => {
if (data) {
setAdjustedData(adjustOrder(data));
}
}, [data]);
const incrementPhase = () => {
setPhase((prevPhase) => prevPhase + 1);
};
const decrementPhase = () => {
if (phase >= -1) setPhase((prevPhase) => prevPhase - 1);
};
const movePhase = (phase: number) => {
setPhase(phase);
};
return (
<Fragment>
{isLoading && <LoadingModal purpose={"회고 작성을 위한 데이터를 가져오고 있어요"} />}
<PhaseContext.Provider value={{ data: adjustedData ?? defaultData, phase, movePhase, incrementPhase, decrementPhase, spaceId, retrospectId }}>
{phase >= 0 ? <Write /> : <Prepare />}
</PhaseContext.Provider>
</Fragment>
);
}
그리고 이에 따른 값을 전달하기 위해 Provider로 자식 요소를 래핑해주고 패널 관리를 진행했어요. phase라는 변수의 상태 값을 통해 필요한 화면으로 전환할 수 있도록 관련 함수들과 상태에 대한 세팅을 먼저 진행을 해주었습니다. 이렇게 지정을 해주고 자식 요소로 전달을 하게 해준다면 각 화면 별 상태를 따로 관리할 필요 없이 부모 영역에서 상태를 전역적으로 관리를 할 수 있어 상태 관리가 편하다는 장점을 가지고 있어요. 위의 코드에서 보이는 Write 컴포넌트도 서비스에서 회고 작성에 대한 뷰를 담당하고 있는 컴포넌트예요, 하나의 뷰 파일에서 어떻게 다양한 화면들을 관리했는지 아래 코드에서 바로 확인할 수 있습니다.
const { data, incrementPhase, decrementPhase, phase, movePhase, spaceId, retrospectId } = useContext(PhaseContext);
...
..
{data?.questions.map((item) => {
return (
item.order === phase && (
<Fragment key={item.questionId}>
{
{
number: (
<Fragment>
<HeaderProvider>
<HeaderProvider.Description
contents={"사전 질문"}
css={css`
color: #7e7c7c;
`}
/>
<HeaderProvider.Subject contents={item.question} />
</HeaderProvider>
<div
css={css`
width: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`}
>
<WSatisfactionTemplate index={parseInt(answers[item.order].answerContent)} onClick={handleClickSatistfy} />
</div>
</Fragment>
),
range: (
<Fragment>
<HeaderProvider>
<HeaderProvider.Description
contents={"사전 질문"}
css={css`
color: #7e7c7c;
`}
/>
<HeaderProvider.Subject contents={item.question} />
</HeaderProvider>
<div
css={css`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
`}
>
<WAchievementTemplate answer={parseInt(answers[item.order].answerContent)} onClick={handleClickAchivement} />
</div>
</Fragment>
),
plain_text: (
<Fragment>
<HeaderProvider>
<HeaderProvider.Description
contents={`{${item.order - (AdvanceQuestionsNum - 1)}}/${data.questions.length - AdvanceQuestionsNum}`}
css={css`
color: #7e7c7c;
font-weight: 500;
letter-spacing: 0.1rem;
font-size: 1.6rem;
.emphasis {
color: black;
}
`}
/>
<HeaderProvider.Subject contents={item.question} />
</HeaderProvider>
<WDescriptiveTemplate answer={answers[item.order].answerContent} onChange={(e) => handleChange(e)} />
</Fragment>
),
combobox: null,
card: null,
markdown: null,
}[item.questionType]
}
</Fragment>
)
);
})}
초기에 설정을 해두었던 여러 상태 값들을 사용하기 위해 useContext를 통해 상태들을 가져오고, 서비스에서 제공되는 API의 data 구조에 따라 각 순서 별 페이즈를 설정했어요. JSX 안에서 if 문을 통해 분기처리를 하는 것이 반복되는 코드가 많아진다는 생각이 들었고, 관련 상태 값에 따라 다른 렌더링이 될 수 있도록 Switch 문을 통해 로직을 구현할 수 있었어요. 이를 통해 화면 별로 상태를 관리하는 것이 아닌, 한 화면에서 상태를 모두 관리할 수 있고 분기 별 로직 처리를 통해 화면 흐름에 대한 부분들을 보다 쉽게 구현할 수 있었습니다.
댓글