학습 타이머와 문제 진행 상태를 함께 다루는 Voca Study 세션 구조
영단어 학습 세션에서 타이머, 현재 단계, 문제 진행 상태를 분리해 예측 가능하게 관리한 기준을 정리합니다.
문제 상황
영단어 학습 화면에는 여러 상태가 동시에 움직입니다. 현재 주차, 현재 단계, 현재 단어, 퀴즈 진행 상태, 타이머가 함께 바뀝니다.
처음에는 화면 컴포넌트 안에서 필요한 상태를 각각 관리해도 충분해 보였습니다. 하지만 단계가 늘어나면 어떤 상태가 세션 전체에 속하고, 어떤 상태가 현재 문제에만 속하는지 구분하기 어려워집니다.
예를 들어 다음 단어로 넘어갈 때는 퀴즈 선택 상태를 초기화해야 하지만, 타이머는 유지해야 합니다. 반대로 세션을 종료하면 타이머와 진행 상태를 모두 초기화해야 합니다. 이 경계를 정하지 않으면 상태 초기화 누락이 생기기 쉽습니다.
세션 상태와 문제 상태 분리
먼저 상태를 두 층으로 나눴습니다.
type StudySessionState = {
weekId: string;
stepIndex: number;
wordIndex: number;
elapsedSeconds: number;
status: "running" | "paused" | "completed";
};
type QuestionState = {
selectedChoiceId: string | null;
submitted: boolean;
};
세션 상태는 학습 전체가 진행되는 동안 유지됩니다. 문제 상태는 현재 문제 안에서만 의미가 있습니다.
이렇게 나누면 다음 문제로 넘어갈 때 어떤 값을 초기화해야 하는지 명확해집니다.
function goToNextQuestion() {
setSession((session) => ({
...session,
wordIndex: session.wordIndex + 1,
}));
resetQuestionState();
}
타이머는 세션 상태에 남아 있고, 문제 선택 상태만 초기화됩니다.
타이머는 화면 렌더링과 분리
타이머는 매초 바뀌기 때문에 불필요한 렌더링을 만들 수 있습니다. 규모가 작은 화면에서는 큰 문제가 아니지만, 학습 화면처럼 여러 컴포넌트가 붙어 있으면 타이머 변경이 전체 UI를 자주 갱신하게 됩니다.
그래서 타이머 값은 세션에 속하되, 타이머 컴포넌트가 필요한 값만 구독하도록 구성하는 편이 좋았습니다.
상태 관리 라이브러리를 쓰는 경우에도 전체 세션 객체를 통째로 구독하기보다 elapsedSeconds만 선택해서 쓰는 방식이 안전합니다.
const elapsedSeconds = useStudySession((state) => state.elapsedSeconds);
이렇게 하면 문제 선택 상태가 바뀌어도 타이머 컴포넌트는 불필요하게 복잡해지지 않습니다.
단계 이동 규칙
학습 단계는 단순한 페이지 이동이 아니라 세션 안의 상태 전환으로 봤습니다. 다음 단계로 이동할 때는 현재 단계에서만 쓰던 상태를 정리해야 합니다.
- 정의 보기 단계: reveal 상태 초기화
- 의미 확장 단계: 현재 관계 단어 초기화
- 퀴즈 단계: 선택지와 제출 상태 초기화
- 세션 종료: 타이머와 인덱스 초기화
이 규칙을 명시하면 “다음 화면으로 넘어갔는데 이전 선택이 남아 있는 문제”를 줄일 수 있습니다.
정리
학습 서비스에서 상태가 꼬이는 이유는 대부분 모든 상태를 같은 수준에서 다루기 때문입니다. 타이머, 현재 단계, 현재 문제 상태는 수명이 다릅니다.
정리한 기준은 다음과 같습니다.
- 세션 상태와 문제 상태를 분리한다.
- 다음 문제로 넘어갈 때는 문제 상태만 초기화한다.
- 세션 종료 시에는 타이머와 인덱스를 함께 초기화한다.
- 타이머 컴포넌트는 필요한 값만 구독한다.
- 단계 이동 시 이전 단계의 임시 상태를 정리한다.
이 구조를 잡아두면 학습 단계가 늘어나도 상태 초기화 기준을 유지할 수 있습니다. 결국 학습 화면의 품질은 화려한 UI보다, 사용자가 다음 단계로 넘어갈 때 이전 상태가 남지 않는 예측 가능성에서 많이 결정됩니다.