Meditation.tsx
가장 먼저 볼 파일은
Meditation.tsx
파일입니다. 최상단 파일로 다른 하위 컴포넌트들을 순서대로 렌더링하는 역할을 합니다.하위 컴포넌트
Container
페이지 전체를 감싸는 스타일 컴포넌트 입니다.
MeditationLabel
명상을 시작합니다. 편안한 명상되세요. 등 명상 도중 사용자의 안내를 돕기 위한 문구를 표시하는 컴포넌트입니다.
MeditationTimer
명상을 시작, 종료 하고 명상의 남은 시간을 알려주는 컴포넌트입니다.
MeditationCounter
명상을 시작하기 전, 시간을 설정할 수 있는 컴포넌트입니다.
Confirm
명상 끝내기 버튼
을 클릭하면 나타나는 확인창입니다.<Container> <MeditationLabel /> <MeditationTimer /> <MeditationCounter /> {confirmCaptured && ( <Confirm width={329} height={389} emoji={'🧘🏻'} emojiSize={4.5} content={'정말 명상을 끝내시겠어요?'} contentFontSize={1.2} nextPageLink={'/'} /> )} </Container>
알아야 할 점
MeditationCounter
는 시간을 세팅하고, MeditationTimer
는 MeditationCounter
에서 세팅한 시간을 가져와서 사용해야 합니다. 따라서 recoil 로 시간을 전역적으로 다뤄줘야 합니다.MeditationLabel.tsx
사용자의 명상 안내를 돕기 위한 컴포넌트입니다.
필요한 문구를 상수로 처리해놓습니다.
const MEDITATION_NOT_STARTED = '명상 시간을 설정해주세요.'; const MEDITATION_ONGOING = '편안한 명상 되세요.'; const MEDITATION_ENDED = '명상이 종료되었습니다.';
각 상수를 보여주기 위해 label state 를 사용합니다.
const [label, setLabe] = useState(MEDITATION_NOT_STARTED); return ( <StyledLabel>{label}</StyledLabel> )
그리고 각 문구를 트리거할 이벤트를 Counter, Timer, EndButton 등 다른 컴포넌트에서 선언해 놓습니다. (추후 설명)
Label 컴포넌트에서는 각 이벤트가 발생하면 이를 감지하여
setLabel
함수를 통해 새로 label
을 렌더링합니다.useEffect(() => { document.addEventListener('START-MEDITATION', () => { setLabel(MEDITATION_ONGOING); }); document.addEventListener('END-MEDITATION', () => { setLabel(MEDITATION_ENDED); }); return () => { document.removeEventListener('START-MEDITATION', () => { setLabel(MEDITATION_ONGOING); }); document.removeEventListener('END-MEDITATION', () => { setLabel(MEDITATION_ENDED); }); }; }, []);
MeditationCounter.tsx
MeditationTimer
가 더 위에 선언되었지만 유저의 행동은 MeditationCounter
를 거쳐야 하므로 Counter 파일을 먼저 살펴봅니다.state
- time
사용자가 설정한 명상 시간입니다. MeditationTimer 와 공유해야 하는 값이 바로 이 값입니다. 따라서 Recoil 로 설정해줍니다.
- timerStarted
타이머가 시작했을 때 상태를 체크하기 위한 값입니다.
- timerEnded
타이머가 끝났을 때 상태를 체크하기 위한 값입니다.
const [time, setTime] = useRecoilState(meditationTime); const [timerStarted, setTimerStarted] = useState(false); const [timerEnded, setTimerEnded] = useState(false);
useEffect
시작 버튼을 누를 때, 명상이 끝날 때를 이벤트로 관리해줍니다.
- 명상 시작 시
timerStarted
를 true
로 바꾸어줍니다.- 명상 종료 시
timerEnded
를 true
로 바꾸어줍니다.useEffect(() => { const handleStartMeditation = () => { setTimerStarted(true); }; const handleEndMeditation = () => { setTimerEnded(true); }; document.addEventListener('START-MEDITATION', handleStartMeditation); document.addEventListener('END-MEDITATION', handleEndMeditation); return () => { document.removeEventListener('START-MEDITATION', handleStartMeditation); document.removeEventListener('END-MEDITATION', handleEndMeditation); }; }, []);
명상이 시작 되기 전 렌더링
타이머가 시작되기 전 (
!timerStarted
) 엔 아래 사진과 같이 시간을 설정할 수 있는 컴포넌트 보여줍니다.
<SetTimeButton /> // 시간 감소 버튼 <CounterText /> // 세팅할 시간을 보여줌 <SetTimeButton /> // 시간 증가 버튼
{!timerStarted && ( <CounterContainer> <SetTimeButton onClick={() => handleTime(BUTTON_TYPE_SUB)}> <Icon name={'chevron_left'} size={BUTTON_SIZE} color={'white'} /> </SetTimeButton> <CounterText>{`${time / 60} 분`}</CounterText> <SetTimeButton onClick={() => handleTime(BUTTON_TYPE_ADD)}> <Icon name={'chevron_right'} size={BUTTON_SIZE} color={'white'} /> </SetTimeButton> </CounterContainer> )}
handleTime 함수
이전 버튼을 클릭하게 되면
BUTTON_TYPE_SUB
을,다음 버튼을 클릭하게 되면
BUTTON_TYPE_ADD
를 넘겨줍니다.넘어오는 타입에 따라서 시간을 증가하거나 감소하는 함수입니다.
const handleTime = (buttonType: string) => { if (time === 0 && buttonType === BUTTON_TYPE_SUB) { return; } if (buttonType === BUTTON_TYPE_ADD) { setTime(time + FIVE_MINUTES_IN_SECONS); } else { setTime(time - FIVE_MINUTES_IN_SECONS); } };
명상 시작 후 렌더링
명상 시작 후엔 명상을 끝내는 버튼을 보여줘야 하므로 다음과 같이 조건문으로 버튼을 보여줍니다.
return ( // ...명상 시작 전 렌더링 코드 {timerStarted && !timerEnded && <MeditationEndButton />} )
MeditationEndButton.tsx
const setPushed = useSetRecoilState(endButtonPushed);
명 종료 버튼을 누르면 명상을 종료하겠냐고 Confirm 창을 보여주기 위해 recoil 로 state 를 만듭니다.
렌더링 시에는 버튼을 누르면 end 버튼이 클릭되었다는 함수만 붙여주면 됩니다.
handleClick = () => { setPushed(true); }
Meditation root 페이지에서 다음과 같이 호출하기 때문
// Meditation.tsx {confirmCaptured && ( <Confirm width={329} height={389} emoji={'🧘🏻'} emojiSize={4.5} content={'정말 명상을 끝내시겠어요?'} contentFontSize={1.2} nextPageLink={'/'} /> )}
MeditationTimer.tsx
렌더링
- hover 시엔 플레이버튼 & 일시정지 버튼을 보여줘야 하기 때문에
hovered
를 state 로 만들어서 관리합니다.
- hover 중에서도
paused
가true
일 땐 플레이버튼, 아닐 경우 일시정지 버튼을 보여줍니다.
- hover 가 아니라면 남은 시간을 보여줍니다.
<TimerElement onClick={() => toggleTimer()} onMouseOver={() => setHovered(true)} onMouseLeave={() => setHovered(false)}> <IconContainer> {hovered ? ( <Icon name={paused ? 'play_arrow' : 'pause'} size={70} color={'white'} /> ) : ( formatTime(time) )} </IconContainer> </TimerElement>
상태
따라서 필요한 상태는 다음과 같습니다. meditationTime 은 Counter 에서도 설정할 수 있어야 하기 때문에 recoil 로 관리합니다.
const [time, setTime] = useRecoilState(meditationTime); const [paused, setPaused] = useState(true); const [hovered, setHovered] = useState(false);
타이머 클릭 이벤트 - toggleTimer()
- 타이머가 정지된 상태가 아니라면 (
!puased
) 타이머를 정지 시킵니다.
그리고 정지상태 (paused) 를
true
로 설정합니다.- 타이머가 정지된 상태라면 타이머를 재시작합니다.
const toggleTimer = () => { if (!paused) { clearInterval(timerId); setPaused(true); } else { startTimer(); } };
타이머 재시작 - startTimer()
타이머 시간을 1초씩 감소시키는 함수입니다.
const startTimer = () => { if (time === 0) { // 시간이 0초일 경우 return return; } setPaused(false); // 정지상태를 취소 // setInterval 함수를 통해 1초마다 감소 timerId = setInterval(() => { // ... }, 1000); // 타이머가 시작되었다는 이벤트를 발생 document.dispatchEvent(new Event('START-MEDITATION')); };
1초마다 실행되는 setInterval 내부 콜백은 다음과 같습니다.
preveTime > 0
시간이 남은 경우 시간을 1초씩 감소시킵니다.
- 시간이 종료된 경우 타이머를 취소하고, 타이머가 끝났다는 이벤트를 발생시킵니다.
setTime((prevTime) => { if (prevTime > 0) { return prevTime - 1; } clearInterval(timerId); document.dispatchEvent(new Event('END-MEDITATION')); return prevTime; });