React.useEffect 흐름 정리 [복기]

박상하·2025년 5월 29일

1년차

목록 보기
17/26

헷갈렸던 코드 흐름

Ivs Player 로직을 짜면서 헷갈렸던 코드 흐름이 있다.

Chat 부분인데 코드를 보면

const MvpPage = () => {
    const { tokenInfo } = useGetIvsToken();
    if (!tokenInfo) {
        return;
    }

    return (
        <Container>
            <Player tokenInfo={tokenInfo} />
            <Chat tokenInfo={tokenInfo} />
        </Container>
    );
};

최상위 컴포넌트이고 Chat 컴포넌트는

function Chat({ tokenInfo }: Props) {
    const [chat, setChat] = React.useState<ChatProps>({ name: ``, content: ``, messageId: ``, senderId: `` });
    const [chatList, setChatList] = React.useState<null | ChatProps[] | null>(null);
    const roomRef = React.useRef<ChatRoom | null>(null);
  
    useConnectChat(roomRef, tokenInfo, setChatList);
    const manageFn = useGetManageFn(roomRef, client, setChatList, setChat);

    return (
        <Container>
            <ChatList chatList={chatList} />
            <ChatForm manageFn={manageFn} chat={chat} setChat={setChat} />
        </Container>
    );
}

이고 내부 useConnectChat와 useGetManageFn은 각각

function useConnectChat(
    roomRef: React.MutableRefObject<ChatRoom>,
    tokenInfo: ReceivedIvsToken | null,
    setChatList: React.Dispatch<React.SetStateAction<ChatProps[]>>,
) {
    const user = useAppSelector(state => state.user.user);

    React.useEffect(() => {
        if (roomRef.current) {
            console.log('⛔ 이미 채팅 방이 초기화됨');
            return;
        }
        if (user.id === 0) {
            console.log('사용자 정보를 불러오는중');
            return;
        }
        const result = connectChat(tokenInfo, setChatList, user);

        if (result) {
            const { room, cleanup } = result;
            roomRef.current = room;
            return () => cleanup();
        }
    }, []);
}
function useGetManageFn(
    roomRef,
    client,
    setChatList,
    setChat,
    setBanList?: React.Dispatch<React.SetStateAction<string[]>>,
) {
    const [manageFn, setManageFn] = React.useState(undefined);

    React.useEffect(() => {
        if (roomRef.current) {
            const manageFn = createChatManager({
                room: roomRef.current,
                client,
                channelArn: CHANNEL_ARN,
                setChatList,
                setBanList,
                setChat,
            });

            setManageFn(manageFn);
        }
    }, []);
    return manageFn;
}


export { useGetManageFn };

내 생각

나의 생각으로

일단 manageFn이 undefined 될 수도 있을 것이라 생각했다.
그렇게 생각한 이유는 useGetManageFn에서 React.useEffect의 의존성 배열은 []로 비어있다.
그렇기 때문에 딱 한 번 실행된다
. (이 딱 한 번에 꽂혀서 생각한게 실수를 유발했다)
그랬을 때 만약 최초에 roomRef.current는 비어있을텐데
그럼 무조건 undefined를 return 하지 않을까?

useEffect 동작

useEffect는 일단 마운트 된 이후 동작한다.
그러니까 내부 컴포넌트들이 다 그려진 후에 동작한다는 것이다.

그랬을 때 물론 최초에 해당 컴포넌트에서 console.log(manageFn)을 찍으면 undefined가 찍힐 것이다.

왜냐하면 그때는 아직 모든 useEffect가 아직 실행이 안되어있으니까!

그럼 useEffect는 최상위부터 하나씩 실행이된다. 즉, 하위 컴포넌트가 모두 실행이되면 그때 useEffect는 위에서 아래로 쭉 실행이된다. 순차적으로.

  • 만약 useeffect cleanup 함수가 있다면 해당 함수는 페이지가 언마운트 될 때 즉, 페이지에서 다른 페이지로 이동할 때 실행이된다.

자 그러면 다시 코드로 돌아가보자

코드로 설명

아까 전에 useConnectChat 내부의 useEffect 함수가 실행되는 시점은 결국 하위 모든 컴포넌트가 그려진 후 상위의 useEffect 내부 함수들이 모두 실행된 후 그리고 차례를 받는다.

그때 내부의 useEffect가 실행된다.

 React.useEffect(() => {
        if (roomRef.current) {
            console.log('⛔ 이미 채팅 방이 초기화됨');
            return;
        }
        if (user.id === 0) {
            console.log('사용자 정보를 불러오는중');
            return;
        }
        const result = connectChat(tokenInfo, setChatList, user);

        if (result) {
            const { room, cleanup } = result;
            roomRef.current = room;
            return () => cleanup();
        }
    }, []);

바로 이 함수다. connectChat 함수는 다음과 같다.

function connectChat(
  tokenInfo: ReceivedIvsToken,
  setChatList: SetChatList,
  currentUser: any
) {
  if (!tokenInfo) {
    return;
  }

  const room = new ChatRoom({
    regionOrUrl: "ap-northeast-2",
    tokenProvider: () => tokenProvider(tokenInfo),
  });

  const unsubscribeOnConnecting = room.addListener("connecting", () =>
    console.log("연결중")
  );
  const unsubscribeOnConnected = room.addListener("connect", () =>
    console.log("연결됨")
  );
  const unsubscribeOnDisconnected = room.addListener("disconnect", () => {});
  const unsubscribeOnMessageReceived = room.addListener(
    "message",
    (message) => {
      const content = message.content;
      const name = message.sender.attributes.nickname;
      const messageId = message.id;
      const senderId = message.sender.userId;

      setChatList((current) => {
        const newMessage = { name, content, messageId, senderId };
        return current ? [...current, newMessage] : [newMessage];
      });
    }
  );
  const unsubscribeOnEventReceived = room.addListener("event", (event) => {
    console.log(event);
  });
  const unsubscribeOnMessageDelete = room.addListener(
    "messageDelete",
    (event) => {
      const deletedMessageIds = event.messageId;
      setChatList((current) =>
        current
          ? current.filter((item) => item?.messageId !== deletedMessageIds)
          : null
      );
    }
  );
  const unsubscribeOnUserDisconnect = room.addListener(
    "userDisconnect",
    (event) => {
      if (event.userId === String(currentUser.id)) {
        window.location.href = "/";
        alert("채팅방에서 퇴장당했습니다.");
      }
    }
  );

  room.connect();

  const cleanup = () => {
    room.disconnect();
    unsubscribeOnConnected();
    unsubscribeOnConnecting();
    unsubscribeOnDisconnected();
    unsubscribeOnEventReceived();
    unsubscribeOnMessageDelete();
    unsubscribeOnMessageReceived();
    unsubscribeOnUserDisconnect();
  };

  return { room, cleanup };
}

길지만 결국엔 동기적 함수이다.

그렇다는건 순서가 분명히 지켜진다는 것

그럼 다시 useConnectChat으로 와서
result가 connectChat에 의해 할당이 되면 이제 roomRef는 current에 room을 담는다.

이제 roomRef는 room이 담겨진 상태이다.

그 후 순차적으로 manageFn을 호출하는 useGetManageFn의 내부 useEffect가 실행된다.

   React.useEffect(() => {
        if (roomRef.current) {
            const manageFn = createChatManager({
                room: roomRef.current,
                client,
                channelArn: CHANNEL_ARN,
                setChatList,
                setBanList,
                setChat,
            });

            setManageFn(manageFn);
        }
    }, []);
    return manageFn;

위 함수에서 roomRef.current는 당연히 담겨있을 것이다. (해당 함수의 오류만 없다면)
그럼 이제 useEffect 함수는 유의미하게 동작하여 manageFn을 return 할 수 있다.

함정 존재

그런데 무조건 useEffect가 부모에서 자식의 흐름으로 순차적으로 실행되는건 아니야

대체적으로 그렇다 는 거지 무조건은 아니다. React는 내부적으로 렌더링 트리순서에 맞춰 useEffect를 실행하지만 이 순서가 절대적이진 않다. 복잡한 상황, 비동기 처리, React 버전 및 최적화 방식에 따라 달라질 수 있다고 한다.

하지만 대체적으로 그렇고 필자는 동기적 코드였기 때문에 그대로 작동해서 문제가 없었던 것 같다.

정리

이런 흐름을 이해하고 코드를 설계해야하고 또 그 이외의 상황이 닥쳤을 때를 대비해
방어코드를 짜놓아야겠다.

부모->자식으로 쭉 렌더링 먼저 그 후
useEffect 부모 -> 자식 순으로 실행(대체적으로 완전한건 아님)

즉, useEffect의 실행시점은 마운트 이후이다. 이 점을 알고 코드를 짜보자

0개의 댓글