const PostDetails = () => {
const { postId } = useParams();
const data = getPostDetails({ postId });
console.log(data); // promise
return (
...
);
};
getPostDetails는 포스트 정보를 가져오는 함수로서 axios 비동기 통신을 제어하기 위해 async 함수로 만들었다. 그런데 async 함수이기 때문에 내부에서는 무조건 promise를 반환한다는 사실을 잊고 왜 안 되는지 계속 고민했다.
const PostDetails = async () => {
const { postId } = useParams();
const data = await getPostDetails({ postId });
console.log(data); // promise
return (
...
);
}
컴포넌트 함수를 async 함수로 만들어봤지만 다음과 같은 두 가지 에러를 내뿜었다.
Unhandled Rejection (Error): Invalid hook call. Hooks can only be called inside of the body of a function component.
Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
(안 되는 정확한 이유는 더 공부해봐야 한다.)
const PostDetails = () => {
const [details, setDetails] = useState({});
const getPostDetailsAsync = useCallback(async () => {
const { postId } = useParams();
const data = await getPostDetails({ postId });
setDetails(data);
}, []);
useEffect(() => {
getPostDetailsAsync();
}, [postId]);
console.log(details); // {}
return (
...
);
};
처음 PostDetails 함수가 실행될 때는 useEffect Hook의 effect 함수에 있는 getPostDetailsAsync가 실행되고, details 객체에 데이터가 담긴다. 상태가 변경되었기 때문에 PostDetails 함수는 다시 렌더링을 하는데, 이 때 postId가 변경되지 않았기 때문에 effect가 실행되지 않고 남은 console.log와 return문을 실행하고 종료하게 된다. 따라서 결과적으로 보면 console.log(details)를 통해 빈 객체 {}가 한 번 찍히고, 그 다음 데이터가 담긴 객체가 찍히는 것을 확인할 수 있다. (여기서 postId를 useEffect의 dependency로 넣은 이유는 url을 통해 postId가 변경될 때 그것을 감지하고 그에 맞는 Details를 다시 요청하기 위함이다.)
const PostDetails = () => {
const [details, setDetails] = useState({});
const [elapsedTime, setElapsedTime] = useState('');
const getPostDetailsAsync = useCallback(async () => {
const { postId } = useParams();
const data = await getPostDetails({ postId });
setDetails(data);
setElapsedTime(elapsedTimeValue);
}, []);
useEffect(() => {
getPostDetailsAsync();
}, [postId]);
useEffect(() => {
if (!details.createdAt) {
return;
}
console.log(details.createdAt);
const elapsedTimeValue = calcElapsedTime(details.createdAt);
console.log(elapsedTimeValue);
}, [details]);
return (
...
);
};
좀 더 자세히 보면 getPostDetailsAsync는 마이크로 태스크 큐에 등록만 되고, 나머지 문들이 실행된다. 콜 스택이 비었을 때 getPostDetalsAsync가 실행되는데, setDetails가 호출되면 상태가 변경되므로 리렌더링 된다. 그럼 또 다시 함수 내부의 문이 실행되는데, postId는 그대로이므로 첫 useEffect는 실행 안 되고, 그 다음 useEffect에서는 details에 데이터가 담긴 상태로 변경되었으므로 Effect가 실행된다.
const PostDetails = () => {
const [details, setDetails] = useState({});
const getPostDetailsAsync = useCallback(async () => {
const { postId } = useParams();
const data = await getPostDetails({ postId });
setDetails(data);
setElapsedTime(elapsedTimeValue);
}, []);
const elapsedTimeValue = useMemo(() => calcElapsedTime(details.createdAt), [
details
]);
useEffect(() => {
getPostDetailsAsync();
}, [postId]);
const elapsedTimeValue = calcElapsedTime(details.createdAt);
console.log(elapsedTimeValue); // '23분 전'
}
이전 코드를 개선하려면 렌더링 사이에 elapsedTimeValue가 다시 연산되지 않게 하기 위해서 (details가 변경되었을 때만 다시 연산하게 하기 위해서) useMemo를 사용할 수 있다.