socket.io를 사용하여 알림 기능 구현 중 아래와 같은 오류가 발생했다.
toast를 이용하여 알림 메시지를 띄워주려고 하는데, const { showToast } = useToast();
부분에서 계속 오류가 발생했다. 커스텀 훅인 useToast
에서 showToast
값을 구조 분해 할당하려 했지만, 해당 값이 undefined
여서 발생하는 문제이다.
toastContext
초기값 설정
// useToast.jsx
const defaultToastValue = {
toast: { show: false, message: '' },
showToast: () => {},
};
const ToastContext = createContext(defaultToastValue);
이 외에도 useToast
가 showToast
를 정상적으로 반환하고 있는지 확인, ToastProvider
의 값 확인, 호출 방식 확인, import 경로 확인, 캐시 삭제, 서버 종료 후 재시작 등등을 시도해보았지만 오류의 원인을 찾을 수가 없었다.
한가지 내가 놓친 부분이 있었는데 바로 아래 코드다.
// App.jsx
<QueryClientProvider client={queryClient}>
<ToastProvider>
<div className='App'>
<Layout>
<Router />
</Layout>
</div>
</ToastProvider>
</QueryClientProvider>
ToastProvier
가 App
을 감싸주고 있어서 아무런 문제가 없다고 생각했는데, 그게 아니었다. 해당 코드는 ToastProvier
가 App
내부에서 감싸져 있어, App
컴포넌트 내부와 그의 자식 컴포넌트들에서만 ToastContext
에 접근할 수 있다. 나는 App
컴포넌트에서 useSocketManager
를 사용하고, 이 훅 내에서 useToast
가 호출되기 때문에 useToast
훅이 올바른 컨텍스트 값을 찾을 수 없었던 것이다.
// index.js
<RecoilRoot>
<ToastProvider>
<App />
</ToastProvider>
</RecoilRoot>
App
외부에서 즉, 상위 컴포넌트인 index.js
에서 App
컴포넌트 자체를 감싸주었다. 이렇게 수정 하면 App
컴포넌트와 그의 자식 컴포넌트들이 ToastContext
에 접근할 수 있다.
Context API의 작동 원리:
React의 Context API는 상위 컴포넌트에서 하위 컴포넌트까지 데이터를 전달하는 방법을 제공합니다. 데이터는 Provider라는 컴포넌트로 전달되며, 그 아래의 컴포넌트 트리에서 해당 데이터를 접근할 수 있습니다. useContext나 Consumer와 같은 메커니즘이 이 데이터에 접근하는 데 사용됩니다.
ToastProvider의 역할:
ToastProvider는 ToastContext의 값을 제공하는 역할을 합니다. 그 안에서만 useToast와 같은 훅을 사용하여 해당 컨텍스트의 값을 사용할 수 있습니다.
문제점:
당신의 원래 코드에서는 App 컴포넌트 안에서 useSocketManager 훅을 사용하였습니다. 그런데 이 훅 내에서 useToast가 호출되고 있었습니다. 문제는 App 컴포넌트가 ToastProvider로 직접 감싸져 있지 않았기 때문에, useToast 훅이 올바른 컨텍스트 값을 찾을 수 없었던 것입니다.
해결책:
App 컴포넌트를 ToastProvider로 감싸주면 App 컴포넌트 내부와 그의 모든 자식 컴포넌트들이 ToastContext에 접근할 수 있게 됩니다. 따라서 useSocketManager 훅 내의 useToast 호출이 올바른 컨텍스트 값을 얻게 되어 문제가 해결되었습니다.
요약하면, 컴포넌트 트리에서 특정 컨텍스트의 값을 사용하려면 해당 컴포넌트가 해당 컨텍스트의 Provider로 감싸져 있어야 합니다. 이를 보장하지 않으면 undefined나 기타 예상치 못한 값들을 반환하게 됩니다.
ToastContext 문제를 해결하자 또 다른 문제가 발생했다.
위 오류는 객체의 프로퍼티를 변경하려고 할 때 발생하는 오류이다. 서버에서 오는 응답 객체를 변경하는 코드가 있는지 살펴보았으나, 없었다.
socket.on('newUserAdded', (data) => {
console.log('이벤트 발생');
const thumbnailUrl = data.thumbnailUrl;
const groupName = data.groupName;
const groupId = data.groupId;
const userId = data.userId;
.
.
.
예를 들면 위와 같은 코드에서 data.thumbnailUrl = "newUrl";
이런 식으로 직접 변경하려고 하면 오류가 발생할 수 있다.
라이브러리 버전 확인, socket.emit()
, socket.on()
등의 메소드에서 추가적인 수정이나 객체 변경이 있는지 확인했으나 오류가 될만한 코드를 찾지 못해서, 코드를 하나하나 디버깅해보기로 했다. 우선 리코일을 사용하는 부분을 주석 처리하자 오류가 사라졌다(?)
import { useSetRecoilState } from 'recoil';
import { socketState, isConnectSocket } from '../recoil/Atom.js';
const setSocket = useSetRecoilState(socketState);
const setIsConnectSocket = useSetRecoilState(isConnectSocket);
const socket = socketIoClient(ENDPOINT, {
withCredentials: true,
});
socket.on('connect', () => {
setSocket(socket);
setIsConnectSocket(true);
console.log('연결 상태', socket.connected);
.
.
.
그래서 리코일 설정 확인, atom, selector 정의 확인, 리액트 버전과 리코일 버전 확인을 해보았으나 여전히 아무런 문제가 없었다.
하나하나 디버깅을 해보다가 setSocket(socket);
이 부분에서 오류가 발생하는 것을 확인했다. socket 객체는 변경 불가능한 프로퍼티들을 포함하기 때문에 리액트 상태관리 도구에 저장할 수 없다고 한다.
socket.on('connect', () => {
SetIsConnectSocket(true);
console.log('연결 상태', socket.connected);
기존 코드에서 socket를 저장하는 부분만 제거해주었다.
Socket 객체의 저장:
socket 객체를 상태로 저장하려는 시도는 일반적으로 추천되지 않습니다. socket 객체는 직렬화할 수 없는 복잡한 객체로, 상태 관리 도구에 저장하는 것이 아니라 다른 방식으로 관리하는 것이 더 적절합니다.
socket 객체를 직접 저장하려 할 때 발생하는 이슈 중 하나는, 이 객체가 직렬화되기 어렵고 변경 불가능한 프로퍼티들을 포함하기 때문입니다. 이러한 특징 때문에 React의 상태 관리 도구 내에 직접 저장하면 예상치 못한 오류를 발생시킬 수 있습니다.
소켓의 연결 상태나 다른 속성들만 Recoil 상태에 저장하고, 소켓 객체 자체는 저장하지 않습니다. 기본적으로 소켓 객체 자체를 저장하는 것을 피하고, 소켓의 특정 상태나 특성만을 저장하는 것을 권장합니다.
예를 들어, 소켓이 연결되었는지의 여부를 나타내는 isConnected라는 상태만을 Recoil 상태로 관리하면 됩니다. 이렇게 하면 복잡한 소켓 객체를 직접 저장하는 것을 피하면서도 필요한 정보를 Recoil을 통해 관리할 수 있습니다.