1. react-datepicker로 달력을 구현하기로 했다.
현재 진행하고 있는 프로젝트에서 달력으로 날짜를 선택하는 것을 구현해야 했다. 달력을 직접 구현하는 것은 힘들 것 같아서 ‘react-datepicker’ 라이브러리를 사용했다. 일단 달력 UI를 피그마에 맞게 커스텀하는 것에 집중했다. 달력을 커스텀하는 것은 생각보다 어려웠지만, 여러 번 시행착오를 겪으니 비슷하게 보이게 만드는데에는 성공할 수 있었다.
2. 이제 날짜 상태를 관리하자
이제는 선택한 날짜를 상태로 관리해야 한다.
먼저 구조를 설명하자면, SelectBox를 클릭하면 내가 만들어놓은 Dialog 컴포넌트가 나오고, Dialog 컴포넌트의 children에 Datepicker가 있고 그 밑에는 확인(confirm) 버튼과 닫기(cancel) 버튼이 있다.
우선 나는 selectedDate(선택된 날짜) 상태와 tempDate(임시 날짜 → Datepicker에서 선택 중인 날짜) 상태를 생성해서 ‘확인’ 버튼을 누르면 tempDate를 selectedDate로 선택하게 했다.
const SelectBox = ({ onChange }: SelectBoxProps) => {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [tempDate, setTempDate] = useState<Date | null>(null);
const [isOpen, setIsOpen] = useState(false);
const { openModal, closeModal } = useModal();
const modalState = useContext(ModalStateContext);
const handleDateChange = (date: Date | null) => {
console.log("Date changed in DatePicker:", date);
setTempDate(date);
};
const handleConfirm = useCallback(() => {
console.log("Confirming date:", tempDate);
if (tempDate) {
setSelectedDate(tempDate);
if (onChange) {
onChange(tempDate);
}
}
setIsOpen(false);
closeModal();
}, [tempDate, onChange, closeModal]);
const handleCancel = useCallback(() => {
setTempDate(null);
setIsOpen(false);
closeModal();
}, [closeModal]);
(...)
하지만 문제가 생겼다. 날짜를 고르고 ‘확인’ 버튼을 눌러도, selectedDate는 null로 나온다는 것이었다. 즉, 바로 반영이 안되었다. console.log로 확인해봤을 때 tempDate는 날짜가 잘 출력이 되어서 tempDate가 selectedDate로 전달이 잘 안되는 구나라고 유추할 수 있었다.
3. async/await를 써볼까
async/await를 도입하면 문제를 해결할 수 있을까. 한 번 도입해봤다.
const handleConfirm = useCallback(async () => {
console.log("Confirming date:", tempDate);
// 상태 업데이트를 기다립니다
await new Promise<void>(resolve => {
setSelectedDate(tempDate);
resolve();
});
if (onChange) {
onChange(tempDate);
}
setIsOpen(false);
closeModal();
}, [tempDate, onChange, closeModal]);
setSelectedDate(tempDate)가 먼저 실행되고 나서, 부모 컴포넌트의 onChange 콜백을 호출했다.
이제 잘되지 않을까? 하고 희망을 가졌었다. 바뀌기는 바뀌었는데.. 조금 이상하게 바뀌었다.
로그를 찍어보니
Date changed in DatePicker: Sun Aug 11 2024 00:00:00 GMT+0900 (한국 표준시) Confirming date: null
Date changed in DatePicker: Thu Aug 22 2024 00:00:00 GMT+0900 (한국 표준시) Confirming date: Sun Aug 11 2024 00:00:00 GMT+0900 (한국 표준시)
Date changed in DatePicker: Wed Aug 07 2024 00:00:00 GMT+0900 (한국 표준시) Confirming date: Thu Aug 22 2024 00:00:00 GMT+0900 (한국 표준시)
Date changed in DatePicker: Sat Aug 03 2024 00:00:00 GMT+0900 (한국 표준시) Confirming date: Wed Aug 07 2024 00:00:00 GMT+0900 (한국 표준시)
보면 알겠지만 날짜가 하루씩 밀리게 되었다!! 왜 이런 일이 일어나게 되었을까? AI는 상태 업데이트의 비동기적 특성과 클로저 문제라고 간단하게 언급해주었다. 아무튼, async/await 말고 다른 방법을 사용해야 한다.
4. useRef를 써보자
AI는 ‘useRef’를 사용해보라고 추천했다. 그러면 코드를 바꿔보자
일단 tempDate 상태는 삭제해주었다. 그대신 tempDateRef를 도입했다.
// const [tempDate, setTempDate] = useState<Date | null>(null);
const tempDateRef = useRef<Date | null>(null);
그리고 각 함수에 tempDateRef를 적용했다.
const handleDateChange = (date: Date | null) => {
console.log('Date changed in DatePicker:', date);
tempDateRef.current = date;
};
const handleConfirm = useCallback(() => {
console.log('Confirming date:', tempDateRef.current);
setSelectedDate(tempDateRef.current);
if (onChange) {
onChange(tempDateRef.current);
}
setIsOpen(false);
closeModal();
}, [onChange, closeModal]);
const handleCancel = useCallback(() => {
tempDateRef.current = null;
setIsOpen(false);
closeModal();
}, [closeModal]);
useState 대신 useRef를 적용하니 날짜 적용이 잘 되었다!
물론 의도하지 않은점이 하나 있었는데, handleCancel의 ‘tempDateRef.current = null’로 설정했던 것은, ‘닫기’ 버튼을 눌렀을 때 그냥 날짜가 초기화되는 것을 기대했던 거였다. 하지만 그냥 SelectBox에 이전에 선택한 날짜가 표시되었다. 사실 이것도 괜찮긴한데, 왜 날짜가 그대로 표시되는 건지 궁금해서 AI에게 물어봤다.
5. AI의 답변
AI는 이렇게 답변했다.
좋은 관찰이십니다! 이 현상의 이유를 설명해 드리겠습니다:
- 클로저(Closure)와 비동기 실행: handleCancel 함수는 모달이 닫힐 때 비동기적으로 실행됩니다. 이 시점에서 tempDateRef.current를 null로 설정하지만, 이미 handleConfirm 함수가 이전에 실행되어 날짜를 설정했을 수 있습니다.
- 상태 업데이트의 순서: React의 상태 업데이트는 비동기적으로 일어나며, 배치(batch) 처리됩니다. handleCancel에서 tempDateRef.current를 null로 설정하더라도, 이는 다음 렌더링 사이클에서 반영됩니다.
- selectedDate 상태의 독립성: tempDateRef는 임시 선택을 위해 사용되고, selectedDate는 최종 선택된 날짜를 저장합니다. handleCancel에서 tempDateRef를 초기화해도 selectedDate는 변경되지 않습니다.
- 모달 닫힘 로직: 모달이 닫힐 때 tempDateRef를 초기화하지만, 이는 다음번 모달을 열 때 사용될 초기값에만 영향을 미칩니다. 이미 선택되고 확인된 날짜(selectedDate)는 그대로 유지됩니다.
- useRef의 특성: useRef의 값을 변경해도 컴포넌트의 리렌더링을 트리거하지 않습니다. 따라서 tempDateRef.current를 변경해도 즉시 UI에 반영되지 않습니다.
이 동작이 의도한 대로라면 좋습니다! 사용자가 날짜를 선택하고 취소해도 이전에 선택한 날짜가 유지되는 것이 UX 관점에서 더 좋을 수 있습니다. 만약 취소 시 날짜를 완전히 초기화하고 싶다면, handleCancel 함수에서 setSelectedDate(null)을 추가로 호출하면 됩니다.
이 방식은 사용자가 실수로 모달을 닫아도 이전 선택을 유지할 수 있어 편리합니다. 필요에 따라 동작을 조정할 수 있으니, 현재 동작이 요구사항에 맞다면 그대로 유지하셔도 좋습니다.
5번 설명이 가장 큰 이유인것 같았다.
그러면 그냥 ‘tempDateRef.current = null;’ 이 문구는 삭제해도 괜찮을 것 같아서 삭제했다.
결론: react-datepicker에서 날짜를 관리할 때는 ‘useRef’를 사용하자!
'Frontend > React' 카테고리의 다른 글
프로젝트 팀원 코드를 보고 컴포넌트 만드는 법 배우기(typescript + clsx) (0) | 2024.07.21 |
---|---|
[React, TS] 프로젝트를 진행하며 상태 관리에 대해 알게 된 것들 (1) | 2024.01.13 |
[React] useEffect()에 대해서 (0) | 2023.08.17 |
[React] Props에 대해서 (0) | 2023.08.17 |
[React] useState()에 대해서 (0) | 2023.08.17 |