문제 상황

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.

(안 되는 정확한 이유는 더 공부해봐야 한다.)

해결 방법

Untitled

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를 사용할 수 있다.