useEffect 콜백에 이름을 붙인다면
- 코드리뷰
- 방법론
들어가며
useEffect 콜백에 이름을 붙여야 할까?
리뷰에서 useEffect 콜백에 이름을 붙여보라는 피드백을 받았다. 이름을 붙이면 코드를 읽기 전에 이 effect가 무엇을 하는지 더 빨리 파악할 수 있다는 설명이었지만, 어차피 한 번만 쓰이고 끝나는 함수에 이름이 왜 필요한지 처음엔 와닿지 않았는데, 직접 바꿔보니 느낌이 조금 달랐다.
이름을 붙이고 나니 코드를 보는 순서 자체가 바뀌었다. 전에는 본문을 끝까지 읽어야 알 수 있던 effect의 목적이, 이제는 이름 하나만 보고도 짐작이 됐다. 그리고 effect가 하나둘 늘어날수록 이 차이는 점점 크게 느껴졌다.
이름만 붙여도 읽는 방식이 달라진다
동작은 같지만 읽는 방식은 달라진다.
우리가 흔히 쓰는 useEffect는 익명 함수 형태다.
// AS-IS: 익명 함수
useEffect(() => {
if (!userId) return;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);이 코드는 userId가 있을 때 사용자 정보를 가져와 setUser로 저장한다. 동작 자체는 단순하지만, 코드를 처음 읽는 사람은 본문을 따라가며 이 effect의 역할을 직접 해석해야 한다. 목적이 먼저 주어지지 않으니, 로직을 다 읽고 나서야 "사용자 정보를 가져오는 effect였구나"라고 거꾸로 알아내야 한다.
같은 내용을 기명 함수로 바꿔보면 이런 형태가 된다.
// TO-BE: 기명 함수
useEffect(
function fetchUserProfile() {
if (!userId) return;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
},
[userId],
);이름을 붙인다고 로직이 달라지는 것은 아니다. 다만 거꾸로 알아내야 했던 목적을, 이제는 선언부에서 먼저 짐작할 수 있게 된다. 읽는 사람은 본문을 열기 전에 먼저 이름을 보고, 필요할 때만 내부 로직을 확인하면 된다.
이름이 여러 effect를 구분하는 표지가 된다
익명 함수가 여러 개면 어떨까?
useEffect가 하나만 있을 때는 이름이 없어도 크게 아쉽지 않다. 하지만 컴포넌트 하나에 useEffect가 두세 개 붙기 시작하면, 어떤 effect가 무슨 역할인지 구분하는 것부터 문제가 된다.
실제로 UserProfile 컴포넌트에는 사용자 정보를 가져오는 effect, 문서 제목을 바꾸는 effect, 페이지 조회를 로깅하는 effect, 이렇게 세 개가 함께 있다고 해보자. 익명 함수로만 두면 세 블록이 나란히 쌓일 뿐이라, 각각이 어떤 역할인지 내부 코드를 읽어야 구분할 수 있다. effect 하나를 읽고 목적을 거꾸로 알아내던 비용이, 이제 세 번 반복된다.
// AS-IS: 익명 함수 세 개
useEffect(() => {
if (!userId) return;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
useEffect(() => {
if (!user) return;
document.title = `${user.name} 프로필`;
}, [user]);
useEffect(() => {
if (!userId) return;
logPageView("user_profile", { userId });
}, [userId]);반면 기명 함수로 바꾸면 각 effect의 역할이 이름에서 먼저 보인다.
// TO-BE: 기명 함수 세 개
useEffect(
function fetchUserProfile() {
if (!userId) return;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
},
[userId],
);
useEffect(
function syncDocumentTitle() {
if (!user) return;
document.title = `${user.name} 프로필`;
},
[user],
);
useEffect(
function logProfileView() {
if (!userId) return;
logPageView("user_profile", { userId });
},
[userId],
);이름이 생기면 본문을 다 읽기 전에 코드의 구조를 먼저 훑을 수 있다. 어떤 effect가 데이터를 가져오고, 어떤 effect가 문서를 바꾸고, 어떤 effect가 로그를 남기는지 빠르게 구분된다. 세 번 반복되던 "거꾸로 알아내는" 비용이, 이번에는 이름을 읽는 것만으로 한 번에 끝난다.
스택 트레이스에도 이름이 남는다
에러를 볼 때도 이름은 그대로 남는다.
기명 함수는 에러 스택 트레이스만 보고 원인을 좁혀야 할 때 효과가 있다. 코드를 직접 열어볼 수 없는 상황이라 이름이 유일한 단서이기 때문이다. useEffect 안에 console.trace()를 넣고 직접 확인해봤더니, 익명 함수와 기명 함수의 스택이 다르게 찍혔다.
익명 함수의 스택은 파일명과 줄 번호 중심으로 찍힌다.
// AS-IS: 익명 함수
useEffect(() => {
console.trace();
}, []);Error
at <http://localhost:8934/useeffect-name-demo.html:23:32>
at commitHookEffectListMount (react-dom.development.js:23199:28)
at commitPassiveMountOnFiber (react-dom.development.js:24980:13)반면 이름을 붙이면 스택 맨 위에 함수 이름이 남는다.
// TO-BE: 기명 함수
useEffect(function fetchUserProfile() {
console.trace();
}, []);Error
at fetchUserProfile (<http://localhost:8934/useeffect-name-demo.html:32:52>)
at commitHookEffectListMount (react-dom.development.js:23199:28)
at commitPassiveMountOnFiber (react-dom.development.js:24980:13)코드 파일을 바로 열 수 없을 때도 스택에 남은 이름만으로 어떤 effect에서 문제가 났는지 바로 좁혀진다. 본문을 열어야 알 수 있던 목적을, 이번에는 파일조차 열 수 없는 상황에서 이름 하나가 대신 알려준 셈이다.
정리하며
본문을 열어야만 역할을 알 수 있다면 이름을 붙일 이유가 있다.
useEffect에 항상 이름을 붙여야 하는 것은 아니다. 효과가 단순하고 컴포넌트 안에서 하나만 쓰인다면, 구분할 대상도 헷갈릴 이름도 없으니 익명 함수로도 충분하다. 하지만 effect가 여러 개 쌓이거나, 스택 트레이스에서 역할을 바로 알고 싶거나, 코드 리뷰에서 빠르게 구조를 파악해야 한다면 이름이 도움이 된다.
세 상황 모두, 결국 본문을 열어봐야만 역할을 알 수 있는지가 갈림길이었다. 이 effect를 처음 보는 사람이 본문을 열어봐야만 역할을 짐작할 수 있다면 이름을 붙일 가치가 있다. 반대로 이름 없이도 역할이 뻔하다면 굳이 추가하지 않아도 된다.
