FE/React

Recoil 메모리 누수 확인하기

고감귤 2025. 1. 11. 21:12

이전에서 Recoil을 선택한 이유와 장점에 대해 설명했다.

마지막에는 Recoil의 메모리 누수 문제점에 대해 언급하며, 내 프로젝트에서도 이 문제가 발생하고 있는지 파악해보겠다고 했다.

 

결론적으로, 현재 진행 중인 외주 프로젝트에서는 메모리 누수가 발생하지 않았다.

[ 확실히 알게 된 건, atom만 쓴다면 메모리 누수 문제는 없다. ]


메모리 누수가 일어나고 있다고 가정하고, atom의 메모리 사용 상태를 지속적으로 점검했지만,

기존에 사용하던 atom들에서 문제가 발견되지 않았다. [ = 사용되지 않으면서도 메모리에 남아 있는 atom은 없었다. ]

 

상당히 뻘짓을 많이 했다. 메모리 누수가 일어난다고 해서, 없는 누수를 찾으려고 크롬 개발자 도구 메모리 탭 기능들을 몇일이나 봤다.

 

이해를 돕기 위해, 메모리 누수가 발생할 수 있는 상황을 코드로 살펴보자.

// ... 기존에 사용하던 atom들

// 테스트를 위해 추가한 selectorFamily
export const userNameQuery = selectorFamily({
    key: 'UserName',
    get: (userID: number) => async () => {
      return userID+"test";
    },
});


function UserInfo({userID}:any) {
    const userName = useRecoilValue(userNameQuery(userID));
    return <div>{userName}</div>;
}

function Root() {
    const snapshot = useRecoilSnapshot();
    useEffect(() => {
        for (const node of snapshot.getNodes_UNSTABLE()) {
            console.log(node.key, snapshot.getLoadable(node));
        }
        console.log("------------------------");
    }, [snapshot]);

    const [id, setID] = useState(Math.random());
    useEffect(() => {
        const intervalId = setInterval(() => {
            setID(Math.random());
        }, 5000);

        return () => clearInterval(intervalId);
    }, []);
    
    return (
        <>
        <Suspense fallback={<div>Loading...</div>}>
            <UserInfo userID={id} />
        </Suspense>
        </>
    );
}

 

코드에서는 5초마다 새로운 랜덤 값을 userID로 전달하고, 이를 기반으로 userNameQuery selectorFamily가 새롭게 생성된다.
콘솔 로그를 보면, 새로운 값이 할당되면서 이전에 할당되었던 값도 여전히 메모리에 남아 있는 것을 확인할 수 있다.

 

하지만, isLogin과 같은 atom의 경우, 로그인 완료 이후 이전 상태 값인 false에서 바뀐 상태 값 true로 바뀐 것을 확인할 수 있다.

atom의 동작방식은 노드는 메모리에서 유지되고, 값만 바뀌기 때문이다.

 

확인을 위해 코드를 보자.

function Login() {

	useTrackNodeChanges("isLogin");
	const setIsLogin = useSetRecoilState(isLoginAtom);

	// 로그인이 성공적으로 이루어지면 -> setIsLogin(true);
    // ...
    // ...
}

// 동일한 atom 노드인지 확인하는 함수
const useTrackNodeChanges = (atomKey: string) => {
    const prevNodeRef = useRef(null);
    const snapshot = useRecoilSnapshot();
    
    const currentNode = Array.from(snapshot.getNodes_UNSTABLE())
        .find(node => node.key === atomKey);
      
    if (!currentNode) {
        console.log(`Node with key ${atomKey} not found`);
        return;
    }
    const currentValue = snapshot.getLoadable(currentNode).contents;
    
    if (prevNodeRef.current) {
        // 노드 동일성 체크
        const isSameNode = prevNodeRef.current === currentNode;
        console.log({
            message: `Node for ${atomKey}:`,
            isSameNode,
            previousValue: prevNodeRef.current.value,
            currentValue,
            previousNodeReference: prevNodeRef.current,
            currentNodeReference: currentNode
        });
    }
    // 현재 노드를 이전 노드로 저장
    prevNodeRef.current = currentNode;
    prevNodeRef.current.value = currentValue; // 값도 저장
    console.log(prevNodeRef.current);
    console.log("--------------------------------------------------------");
};

로그인이 완료되어 isLogin의 값이 true로 바뀌어도, 같은 노드임을 확인할 수 있다.

 

앞으로, 새로운 Recoil에서 상태 관리에 사용되는 atom 이외의 키워드를 또 도입하게 된다면,
값이 변할 때 새로운 노드를 만들지만 기존의 노드를 삭제하지 않는지,
기존 노드를 유지하는지를 꼭 디버깅 해보는 것이 중요할 것 같다.

 

이 글이 도움되기를 바랍니다. 추가적으로 보완할 점이 있다면 언제든 알려주세요! 🙂

 

참조한 사이트

https://medium.com/@altoo/recoil%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%EB%AC%B8%EC%A0%9C-fb709973acf2

https://github.com/facebookexperimental/Recoil/issues/1840

https://recoiljs.org/ko/docs/api-reference/core/useRecoilSnapshot

https://velog.io/@saewoo1/useRef-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0%EC%9A%94-%EB%84%A4

https://www.youtube.com/watch?v=P3C7fzMqIYg