React18에서 19로 업데이트 하며 가이드를 보던 중, ref 속성에 콜백함수에 클린업 함수를 지원하게 되었으니, 암묵적 반환을 제거하라는 내용을 보았다.

ref 속성에 할당해 실행되는 콜백함수 동작에 대해 깊게 생각해본 적이 없었다. 단순히 DOM 렌더시 인자로 DOM 노드를 넘겨준다고만 알고 있었다. 그래서 왜 19에서 클린업함수를 지원하게 됐는지 궁금해졌고, 공식 문서를 바탕으로 이해한 내용을 정리하고자 한다.
ref 콜백 함수 (callback function)
ref 속성은 특정 DOM 요소에 직접 접근할 수 있도록 해준다. 일반적으로 useRef 훅을 사용해 ref 객체를 생성하고, 이를 ref 속성에 할당하는 방식이 널리 사용된다.
const MyComponent = () => {
const myRef = useRef(null);
return <div ref={myRef}>Hello</div>;
}
ref 속성에는 객체뿐만 아니라 함수를 전달할 수도 있다. 여기에 전달하는 함수를 ref 콜백 함수(callback function)이라고 한다.
const MyComponent = () => {
return (
<div ref={(node) => console.log(node)} />
)
}
React 18에서 ref 콜백 함수 (callback function)
언제 어떻게 실행되나
- 해당 DOM 요소가 화면에 추가될 때
- → ref 콜백이 실행되며, DOM 노드가 인자로 전달됨.
- 해당 DOM 요소가 제거될 때
- → ref 콜백이 null을 인자로 다시 실행됨.
- 위 예제처럼 렌더마다 ref 콜백 함수가 새로 생성 될 때도 매번 실행된다.
- → 기존 콜백이 null로 실행되고, 새로운 콜백에서 DOM 노드가 인자로 전달되어 다시 실행됨
클린업 함수는 왜 추가되었을까
위 방식은 직관적이긴 하지만, div가 제거될 때 클린업 작업이 필요할 경우 아래 예시처럼 node을 확인해 처리해야했다.
return (
<div ref={(node) => {
if (node) {
...
}
else {
...
}
}}>
)
또한 unmount시에 ref 콜백 함수에서는 node가 null로 들어와서, node 에 대한 참조가 필요한 클린업 작업은 어려웠다.
+ 최근에 경험한 예시로, ref 콜백 함수에서 IntersectionObserver 를 생성하고 observe하는 코드를 작성하다가 unmount시 unobserve 처리는 ref 콜백 함수 내에서 불가능하다는 것을 깨닫고 useEffect를 사용한 적이 있었다.
return (
<div ref={(node) => {
const observer = new IntersectionObserver(callback);
if (node) {
observer.observe(document.querySelector(node));
}
else {
// observer.unobserve(node); unmount시에는 node가 null이므로 의도대로 unobserve 되지 않는다.
}
}}>
)
찾아보니 GitHub 이슈에서도 해당 ref에 클린업을 구현해줄 것을 요청하는 글이 있었다.
https://github.com/facebook/react/issues/15176
React callback ref cleanup function · Issue #15176 · facebook/react
At the time React added callback refs the main use case for them was to replace string refs. A lot of the callback refs looked like this: <div ref={node => this.node = node} /> With the introductio...
github.com
토스에서 이 요구사항을 충족해주는 훅을 만들어 슬래시 라이브러리에서 제공해주고 있었다. 만약 19로 업데이트 전이라면, 사용할만 해보인다.
https://www.slash.page/ko/libraries/react/react/src/hooks/useRefEffect.i18n
useRefEffect | Slash libraries
- See//github.com/facebook/react/issues/15176 - ref의 cleanup을 구현해달라고 요청하는 GitHub Issue
www.slash.page
React 19에서 클린업 함수의 추가
React 19에서는 ref callback function에서 cleanup function을 반환할 수 있도록 개선되었다. 즉, ref가 해제될 때 실행할 정리(cleanup) 코드를 직접 작성할 수 있다.
이 변화로 인해 ref가 해제될 때 필요한 정리 작업을 명확하게 처리할 수 있는 구조가 되었다.
return (
<div ref={(node) => {
console.log('Attached', node);
return () => {
console.log('Clean up', node) // 여기서 node의 값은 null 이 아니라 DOM node 이다.
}
}}>
)
React 18과의 차이점
React 18 | React 19 | |
ref가 연결될 때 | ref 콜백이 실행됨 (node 전달) | 동일 |
ref가 해제될 때 | ref 콜백이 null 인자로 실행됨 | cleanup function이 실행됨 (cleanup function 이 없을 경우, 18과 동일하게 null 이 전달된다. 이 동작은 추후 제거 예정 |
렌더링마다 ref 콜백을 새로 만들어 전달할 때 |
기존 콜백이 null을 인자로 실행 후, 새로운 콜백이 실행됨 | 기존 콜백의 cleanup function이 실행된 후, 새로운 콜백이 실행됨 |
DOM 요소를 제거하고 다시 렌더하는 과정을 테스트해보았다.
클린업 함수를 설정하지 않으면 인자로 null이 들어오고, 클린업 함수를 설정하면 인자로 DOM 노드가 들어오는 것을 콘솔을 통해 확인할 수 있었다.
클린업 함수를 설정하지 않았을 때 (React 18)
return (
<div ref={(node) => console.log(node)} />
)

추가로 렌더링시마다 ref callback function을 새로 생성하는 상황에서
컴포넌트 함수 내 다른 state를 변경하여 리렌더링을 발생시켰을 때도 확인해보았다.

클린업 함수를 설정할 때 (React 19)
return (
<div ref={(node) => {
console.log(node);
return () => {
console.log('Clean up', node);
}
}}>
)

컴포넌트 함수 내 다른 state를 변경하여 리렌더링을 발생시켰을 때

마무리하며
React 18의 ref 콜백 함수의 한계를 알아보면서 클린업 함수의 필요성을 체감할 수 있었다. 특히 이전에는 IntersectionObserver를 사용할 때 unobserve를 처리하려면 반드시 useEffect를 사용해야 했는데, 이제는 ref 콜백 함수에서 바로 정리할 수 있어 코드가 훨씬 직관적이고 간결해졌다.
React 19업데이트 이후 앞으로 리팩토링할 때 불필요한 useEffect를 줄이고, ref 콜백을 더 적극적으로 활용할 수 있을 것 같다는 생각이 들었다. 작은 변화 같지만, 실제 개발 경험에서는 꽤 큰 차이를 만들 수 있는 기능이다. 특히 DOM 관찰 로직을 다룰 때 훨씬 더 깔끔한 코드 작성을 가능하게 해준다. React 19로 업데이트 하면서 이런 개선점을 잘 활용하면, 유지보수도 편해지고 불필요한 코드도 줄일 수 있을 것이다.
React18에서 19로 업데이트 하며 가이드를 보던 중, ref 속성에 콜백함수에 클린업 함수를 지원하게 되었으니, 암묵적 반환을 제거하라는 내용을 보았다.

ref 속성에 할당해 실행되는 콜백함수 동작에 대해 깊게 생각해본 적이 없었다. 단순히 DOM 렌더시 인자로 DOM 노드를 넘겨준다고만 알고 있었다. 그래서 왜 19에서 클린업함수를 지원하게 됐는지 궁금해졌고, 공식 문서를 바탕으로 이해한 내용을 정리하고자 한다.
ref 콜백 함수 (callback function)
ref 속성은 특정 DOM 요소에 직접 접근할 수 있도록 해준다. 일반적으로 useRef 훅을 사용해 ref 객체를 생성하고, 이를 ref 속성에 할당하는 방식이 널리 사용된다.
const MyComponent = () => {
const myRef = useRef(null);
return <div ref={myRef}>Hello</div>;
}
ref 속성에는 객체뿐만 아니라 함수를 전달할 수도 있다. 여기에 전달하는 함수를 ref 콜백 함수(callback function)이라고 한다.
const MyComponent = () => {
return (
<div ref={(node) => console.log(node)} />
)
}
React 18에서 ref 콜백 함수 (callback function)
언제 어떻게 실행되나
- 해당 DOM 요소가 화면에 추가될 때
- → ref 콜백이 실행되며, DOM 노드가 인자로 전달됨.
- 해당 DOM 요소가 제거될 때
- → ref 콜백이 null을 인자로 다시 실행됨.
- 위 예제처럼 렌더마다 ref 콜백 함수가 새로 생성 될 때도 매번 실행된다.
- → 기존 콜백이 null로 실행되고, 새로운 콜백에서 DOM 노드가 인자로 전달되어 다시 실행됨
클린업 함수는 왜 추가되었을까
위 방식은 직관적이긴 하지만, div가 제거될 때 클린업 작업이 필요할 경우 아래 예시처럼 node을 확인해 처리해야했다.
return (
<div ref={(node) => {
if (node) {
...
}
else {
...
}
}}>
)
또한 unmount시에 ref 콜백 함수에서는 node가 null로 들어와서, node 에 대한 참조가 필요한 클린업 작업은 어려웠다.
+ 최근에 경험한 예시로, ref 콜백 함수에서 IntersectionObserver 를 생성하고 observe하는 코드를 작성하다가 unmount시 unobserve 처리는 ref 콜백 함수 내에서 불가능하다는 것을 깨닫고 useEffect를 사용한 적이 있었다.
return (
<div ref={(node) => {
const observer = new IntersectionObserver(callback);
if (node) {
observer.observe(document.querySelector(node));
}
else {
// observer.unobserve(node); unmount시에는 node가 null이므로 의도대로 unobserve 되지 않는다.
}
}}>
)
찾아보니 GitHub 이슈에서도 해당 ref에 클린업을 구현해줄 것을 요청하는 글이 있었다.
https://github.com/facebook/react/issues/15176
React callback ref cleanup function · Issue #15176 · facebook/react
At the time React added callback refs the main use case for them was to replace string refs. A lot of the callback refs looked like this: <div ref={node => this.node = node} /> With the introductio...
github.com
토스에서 이 요구사항을 충족해주는 훅을 만들어 슬래시 라이브러리에서 제공해주고 있었다. 만약 19로 업데이트 전이라면, 사용할만 해보인다.
https://www.slash.page/ko/libraries/react/react/src/hooks/useRefEffect.i18n
useRefEffect | Slash libraries
- See//github.com/facebook/react/issues/15176 - ref의 cleanup을 구현해달라고 요청하는 GitHub Issue
www.slash.page
React 19에서 클린업 함수의 추가
React 19에서는 ref callback function에서 cleanup function을 반환할 수 있도록 개선되었다. 즉, ref가 해제될 때 실행할 정리(cleanup) 코드를 직접 작성할 수 있다.
이 변화로 인해 ref가 해제될 때 필요한 정리 작업을 명확하게 처리할 수 있는 구조가 되었다.
return (
<div ref={(node) => {
console.log('Attached', node);
return () => {
console.log('Clean up', node) // 여기서 node의 값은 null 이 아니라 DOM node 이다.
}
}}>
)
React 18과의 차이점
React 18 | React 19 | |
ref가 연결될 때 | ref 콜백이 실행됨 (node 전달) | 동일 |
ref가 해제될 때 | ref 콜백이 null 인자로 실행됨 | cleanup function이 실행됨 (cleanup function 이 없을 경우, 18과 동일하게 null 이 전달된다. 이 동작은 추후 제거 예정 |
렌더링마다 ref 콜백을 새로 만들어 전달할 때 |
기존 콜백이 null을 인자로 실행 후, 새로운 콜백이 실행됨 | 기존 콜백의 cleanup function이 실행된 후, 새로운 콜백이 실행됨 |
DOM 요소를 제거하고 다시 렌더하는 과정을 테스트해보았다.
클린업 함수를 설정하지 않으면 인자로 null이 들어오고, 클린업 함수를 설정하면 인자로 DOM 노드가 들어오는 것을 콘솔을 통해 확인할 수 있었다.
클린업 함수를 설정하지 않았을 때 (React 18)
return (
<div ref={(node) => console.log(node)} />
)

추가로 렌더링시마다 ref callback function을 새로 생성하는 상황에서
컴포넌트 함수 내 다른 state를 변경하여 리렌더링을 발생시켰을 때도 확인해보았다.

클린업 함수를 설정할 때 (React 19)
return (
<div ref={(node) => {
console.log(node);
return () => {
console.log('Clean up', node);
}
}}>
)

컴포넌트 함수 내 다른 state를 변경하여 리렌더링을 발생시켰을 때

마무리하며
React 18의 ref 콜백 함수의 한계를 알아보면서 클린업 함수의 필요성을 체감할 수 있었다. 특히 이전에는 IntersectionObserver를 사용할 때 unobserve를 처리하려면 반드시 useEffect를 사용해야 했는데, 이제는 ref 콜백 함수에서 바로 정리할 수 있어 코드가 훨씬 직관적이고 간결해졌다.
React 19업데이트 이후 앞으로 리팩토링할 때 불필요한 useEffect를 줄이고, ref 콜백을 더 적극적으로 활용할 수 있을 것 같다는 생각이 들었다. 작은 변화 같지만, 실제 개발 경험에서는 꽤 큰 차이를 만들 수 있는 기능이다. 특히 DOM 관찰 로직을 다룰 때 훨씬 더 깔끔한 코드 작성을 가능하게 해준다. React 19로 업데이트 하면서 이런 개선점을 잘 활용하면, 유지보수도 편해지고 불필요한 코드도 줄일 수 있을 것이다.