Custom Hook 만들기 - 1
date-fns를 이용한 시간 표시 기능을 컴포넌트로 추출 후 이를 처리하는 커스텀 훅 만들기
- Datetime Component
- 시간표시(eg. 몇분 전) 기능을 컴포넌트로 따로 추출
import distanceInWords from 'date-fns/distance_in_words_to_now';
import useUpdate from '@/hooks/useUpdate'
// date-fns 함수 호출을 distance라는 함수 내에 넣기. 그래야 distance 함수가 불렸을 때 비로소 실행되도록 할 수 있다.
const distance = (time) => {
distanceInwords(time, {
locale,
addSuffix:true
});
}
// createAt 변수를 time으로 받음
// 최초 상태는 distance(time) 으로 설정
const Datetime = ({time}) => {
const [datetime, setDatetime] = useState(distance(time));
// 커스텀 훅 useUpdate 사용
// setDatetime함수를 콜백으로 넘기면 끝
useUpdate(()=>setDatetime(distance(time)));
return <span>{datetime}</span>
}
export default Datetime;
- useUpdate
- Datetime이 1분마다 갱신되도록 하는 기능을 커스텀 훅으로 구현
import {useEffect} from 'react';
const listeners = [];
const useUpdate = (callback) => {
useEffect(()=>{
if(!listeners.includes(callback) && typeof callback === 'function') {
listeners.push(callback)
}
return () => {
if(listeners.includes(callback)) {
listeners.splice(listeners.indexOf(callback),1);
}
};
}, [callback]);
}
// setTimeout을 사용하면 listener 중 하나가 제대로 작동하지 않아도 나머지 listener 실행을 멈추지 않는다.
setInterval(()=>listeners.forEach(listener => setTimeout(listener)), 60000);
export default useUpdate;
몇 분전
시간 표시 컴포넌트(=Datetime)를 포함하는 포스트, 코멘트 컴포넌트가 마운트된다.
- 자식 컴포넌트인 Datetime 컴포넌트가 마운트된다. 이 때 부모인 포스트와 코멘트로부터 생성된 시간을 의미하는 createAt이라는 변수를 time이라는 이름으로 전달받는다. createAt은 LocalDatetime.now()로 생성된 것이다. (컨텐츠를 클라이언트로부터 전달받으면 백엔드 API가 포스트나 코멘트를 생성하여 응답하는 구조이다)
- 전달받은 time을 date-fns로부터 임포트한
distanceInWords
함수에 아규먼트로 전달하고 이를 datetime
이라는 변수의 초기값으로 설정한다. distanceInWords
함수는 함수명처럼 date 객체를 인자로 받아 n분전, n일전 등의 말로 표시된 결과를 리턴한다.
- 그리고 커스텀 훅인 useUpdate에
()=>setDatetime(distance(time))
함수를 전달한다. distance함수를 따로 설정한 것은 distanceInWords
가 바로 실행되는 것을 막기 위해서다.
- useUpdate 훅은 useEffect를 사용하여 만들었고, 아규먼트로 전달된 콜백함수(이 경우
()=>setDatetime(distance(time))
함수)가 리스너 배열에 추가되도록 되어 있다. 여기서 useEffect의 주요한 일은 이 리스너 배열을 관리하는 것이다.
- 그리고 리스너 배열들에 존재하는 함수들은 setInterval 함수를 통해 1분마다 실행되도록 설정되어 있다.리스너 배열?
-
- 여기서 useUpdate의 리스너 배열의 존재가 중요하다. 이 리스너 배열은 Datetime컴포넌트의 setDatetime함수 즉, 1분마다 컴포넌트에 표시되는 시간을 갱신하는 것을 모두 한꺼번에 모아서 처리하기 위한 것이다.
- setInterval은 1분마다 리스너 배열에 있는 함수들을 하나씩 꺼내서 실행하는 것을 반복한다. 이 함수들은 부모 컴포넌트로부터 온 createAt(time)을 distanceInWords에 아규먼트로 전달한 결과(n분전으로 표시되는 결과)를 업데이트 하는 setDatetime함수이다.
- 일분마다 무조건 setInterval은 시행될 거고, 그때마다 리스너 배열에 모든 포스트 컴포넌트와 코멘트 컴포넌트의 setDatetime이 들어가있는게 중요하다. 포스트 컴포넌트가 1부터 10까지 존재하는데, 그 중 9개 포스트만 Datetime을 처리하는 함수가 리스너에 들어있다면 안되는 것이다.
- 즉 이렇게 Datetime을 포함하고 있는 부모 컴포넌트들의 함수들을 모아놓은 것이 리스너 배열이고, 이렇게 한꺼번에 모아둠으로써 setInterval을 한 번만 돌려도 된다는 이점이 있다.
- 더구나 setTimeout으로 실행하여 비동기처리되므로 중간에 하나가 제대로 실행되지 않아도 나머지의 실행에는 영향을 미치지 않을 것이다.그렇다면 useEffect는 리스너 배열을 관리하는 일을 잘해내고 있는가?
-
- useEffect의 구조를 보면 콜백함수와 클린업함수를 갖고 있고, 아규먼트를 받아서 이를 subscribe하고 있다.
- useEffect의 콜백함수는 컴포넌트가 처음 마운트될 때, 그리고 컴포넌트가 업데이트될 때 실행된다. (componentDidMount, componentDidUpdate)
- useEffect의 클린업 함수(리턴 함수)는 컴포넌트가 업데이트될 때, 그리고 컴포넌트가 언마운트될 때 실행된다. (componentDidUpdate, componentWillUnmount)
- 위에서 말했듯이 리스너 배열에서 중요한 점은 마운트되어 있는 모든 Datetime의 부모 컴포넌트들의 함수가 포함되어야 한다는 점이다. 즉 포스트가 API로부터 fetch되거나, 포스트가 새로 등록돼서 API에 갔다가 돌아왔거나 등등의 액션이 발생했을 때 리스너에 성공적으로 추가되어야 한다는 소리이다. 당연히 언마운트될 때는 리스너 배열에서 삭제돼야 하고.
- 새로운 컴포넌트가 fetch될 때, 해당 컴포넌트의
()=>setDatetime(distance(time))
함수는 새로이 만들어지고 리스너 배열에 추가된다. 당연히 언마운트될 때에는 클린업함수가 실행되어 제거된다.
- 다만 아직 궁금한 점은 comment하나가 fetch될 때 기존에 이미 업데이트가 된 포스트들과 코멘트들의 useEffect가 재실행되는데(정확히는 클린업되어 리스너에서 제거됐다가, 다시 콜백이 실행되며 리스너에 추가된다. componenetDidUpdate된 경우 클린업과 콜백 모두 실행되므로, 컴포넌트가 구독하고 있는 것이 업데이트됐다고 볼 수 있다.), 이것은 subscribe하고 있는 함수가 변경되었다는 소리이다. 홈에서 포스트와 코멘트 컴포넌트를 map으로 풀어서 리턴하고 있긴한데, 그 과정에서 그렇게 되는 것일까? 왜 나머지 함수들이 변경되는지는 좀 더 찾아봐야될 것 같다.
- 이 Datetime 컴포넌트가 작동되는 순서는 다음과 같다