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는 일단 마운트 된 이후 동작한다.
그러니까 내부 컴포넌트들이 다 그려진 후에 동작한다는 것이다.
그랬을 때 물론 최초에 해당 컴포넌트에서 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의 실행시점은 마운트 이후이다. 이 점을 알고 코드를 짜보자