당신의 useEffect에 이름을 붙여야 하는 이유

당신의 useEffect에 이름을 붙여야 하는 이유

태그
React
DX
최종 수정일
Last updated April 3, 2026
Slug
give-a-name-to-useEffect
Date
April 2, 2026
Published
Published
notion image

useEffect에 이름을 붙여야 하는 이유

useEffect의 첫 번째 인자로 화살표 함수를 넘기는 것은 React 생태계의 사실상 표준입니다. 공식 문서도, 대부분의 튜토리얼도 이 형태를 사용합니다. 이 글에서는 그 보편적인 관행에 작은 변화를 제안합니다. 화살표 함수 대신 named function expression을 사용하면 코드 가독성뿐 아니라 디버깅까지 달라집니다.

왜 모두 화살표 함수를 쓰는가

useEffect는 함수를 인자로 받습니다. 화살표 함수든 일반 함수든 동작에는 차이가 없습니다.
두 형태 모두 동일하게 실행됩니다.
useEffect(() => { /* ... */ }, []); useEffect(function setup() { /* ... */ }, []);
화살표 함수가 보편적으로 쓰이는 이유는 단순합니다. 짧고, 익숙하고, React 공식 문서가 이 형태를 사용하기 때문입니다. effect의 본문이 핵심이지, 그것을 감싸는 함수의 형태는 관심사가 아닌 경우가 대부분입니다. 실제로 effect가 하나뿐인 간단한 component에서는 화살표 함수로 충분합니다.
문제는 component가 복잡해질 때 시작됩니다.

이름이 만드는 가독성의 차이

effect가 3~4개 이상인 component를 떠올려 보겠습니다. 화살표 함수만으로 작성된 코드에서는 각 effect의 역할을 파악하려면 본문을 읽어야 합니다.
화살표 함수로 작성된 세 개의 effect입니다. 각 effect가 무엇을 하는지 본문을 읽기 전까지 알 수 없습니다.
function Dashboard() { useEffect(() => { const ws = new WebSocket(url); ws.addEventListener("message", handleMessage); return () => ws.close(); }, [url]); useEffect(() => { trackUserActivity(userId); }, [userId]); useEffect(() => { document.title = `Dashboard — ${userName}`; }, [userName]); return <div>...</div>; }
named function expression을 적용하면, 본문을 읽지 않아도 각 effect의 역할이 드러납니다.
function Dashboard() { useEffect(function connectWebSocket() { const ws = new WebSocket(url); ws.addEventListener("message", handleMessage); return () => ws.close(); }, [url]); useEffect(function trackActivity() { trackUserActivity(userId); }, [userId]); useEffect(function updateDocumentTitle() { document.title = `Dashboard — ${userName}`; }, [userName]); return <div>...</div>; }
코드 리뷰에서 "이 effect 뭐 하는 거예요?"라는 질문이 줄어듭니다. 주석을 달지 않아도 함수 이름 자체가 문서 역할을 합니다. 하지만 이름을 붙이는 이유가 단순히 가독성 때문만은 아닙니다.

가독성을 넘어서: stack trace

화살표 함수는 구문 차원에서 이름을 가질 수 없습니다. 변수에 할당하면 JavaScript 엔진이 변수명을 함수의 name property로 추론하지만, useEffect의 인자로 바로 전달하는 인라인 화살표 함수에는 이 추론이 작동하지 않습니다.
// 변수 할당 — name property가 "setup"으로 추론됨 const setup = () => { /* ... */ }; console.log(setup.name); // "setup" // useEffect 인라인 — 추론 불가, anonymous로 남음 useEffect(() => { /* ... */ }, []);
이 차이가 실질적으로 영향을 미치는 곳은 에러 발생 시 stack trace입니다.
앞서 본 Dashboard component에서 두 번째 effect(trackUserActivity)에 에러가 발생했다고 가정합니다. 화살표 함수로 작성된 경우, 브라우저 console에 다음과 유사한 stack trace가 출력됩니다.
TypeError: trackUserActivity is not a function at anonymous (Dashboard.tsx:12:5) at commitHookEffectListMount (react-dom.development.js:23150:26) at commitPassiveMountOnFiber (react-dom.development.js:24926:11)
anonymous라는 정보만으로는 세 effect 중 어디서 에러가 발생했는지 특정할 수 없습니다. development 환경에서는 파일명과 라인 넘버로 찾을 수 있지만, production 환경에서는 minification으로 인해 라인 넘버마저 의미를 잃습니다. Sentry나 Datadog 같은 에러 모니터링 서비스에서 anonymous가 나열된 stack trace를 마주하면, 원인 파악에 불필요한 시간을 소비하게 됩니다.
named function expression을 적용한 경우의 stack trace입니다.
TypeError: trackUserActivity is not a function at trackActivity (Dashboard.tsx:12:5) at commitHookEffectListMount (react-dom.development.js:23150:26) at commitPassiveMountOnFiber (react-dom.development.js:24926:11)
trackActivity라는 이름이 표시되므로, 파일을 열기 전에 어떤 effect가 문제인지 파악할 수 있습니다. 함수 이름은 minification 이후에도 source map을 통해 복원되기 때문에, production 에러 추적에서도 유효합니다.
좌측은 (익명)으로 시작하나, 우측은 triggerNamedError라는 정확한 이름이 있다.
notion image
notion image
React DevTools에서도 동일한 차이가 드러납니다. Components 탭에서 hook 목록을 확인할 때, 화살표 함수는 단순히 "Effect"로 표시되지만 named function expression은 해당 이름이 함께 표시됩니다. Profiler로 렌더링 성능을 분석할 때도 마찬가지입니다. "Effect"가 세 줄 나열된 것과 "connectWebSocket", "trackActivity", "updateDocumentTitle"이 각각 표시되는 것은 디버깅 효율에서 분명한 차이가 있습니다.

cleanup 함수에도 이름을 붙일 수 있다

useEffect가 반환하는 cleanup 함수 역시 named function expression으로 작성할 수 있습니다. cleanup에서 에러가 발생하는 경우는 상대적으로 드물지만, WebSocket 해제, event listener 정리, timer 취소 등의 로직이 복잡해지면 cleanup 내부에서도 에러가 발생할 수 있습니다.
effect 함수와 cleanup 함수 모두에 이름을 붙인 예시입니다.
useEffect(function connectWebSocket() { const ws = new WebSocket(url); ws.addEventListener("message", handleMessage); return function disconnectWebSocket() { ws.removeEventListener("message", handleMessage); ws.close(); }; }, [url]);
cleanup에서 에러가 발생하면 stack trace에 disconnectWebSocket이 표시되므로, "연결을 끊는 과정에서 문제가 생겼다"는 맥락을 즉시 파악할 수 있습니다.

네이밍 패턴

effect에 붙이는 이름은 "이 effect가 무엇을 하는지"를 동사구로 표현하는 것이 자연스럽습니다. component 내에서 여러 effect의 이름이 나란히 보였을 때, 각각의 역할이 즉시 구분되어야 합니다.
실무에서 자주 사용되는 네이밍 패턴들입니다.
// 연결/구독 계열 useEffect(function connectWebSocket() { /* ... */ }, []); useEffect(function subscribeToNotifications() { /* ... */ }, []); useEffect(function listenToResize() { /* ... */ }, []); // 데이터 fetching 계열 (그치만 요즘 이 패턴은 거의 안 쓰고, Tanstack-Query로 보통 통일되었다고 볼 수 있죠?) useEffect(function fetchUserProfile() { /* ... */ }, [userId]); useEffect(function loadInitialData() { /* ... */ }, []); // DOM 조작 / 외부 시스템 동기화 계열 useEffect(function updateDocumentTitle() { /* ... */ }, [title]); useEffect(function syncToLocalStorage() { /* ... */ }, [settings]); useEffect(function initializeAnalytics() { /* ... */ }, []);
이름은 짧을수록 좋지만, 역할이 모호해질 정도로 축약하면 의미가 없습니다. setup이나 init 같은 범용적인 이름보다는 구체적인 동작을 담는 것이 효과적입니다.

ESLint로 팀 전체에 적용하기

개인 습관에 머무르면 코드베이스 전체에서 일관성을 유지하기 어렵습니다. ESLint를 활용하면 named function expression 사용을 팀 컨벤션으로 강제할 수 있습니다.
ESLint에는 func-names라는 내장 rule이 있습니다. 이 rule을 "as-needed" 또는 "always"로 설정하면 anonymous function expression을 감지하여 경고하거나 에러로 처리합니다. 하지만 func-names는 코드베이스 전체의 모든 function expression에 적용되므로, useEffect 인자에만 한정하기에는 범위가 넓습니다.
useEffect에 대해서만 named function expression을 강제하고 싶다면, eslint-plugin-react-hooks와 조합하여 custom rule을 작성하는 방법이 있습니다.
useEffect의 첫 번째 인자가 anonymous 화살표 함수 또는 anonymous function expression인지를 검사하는 custom ESLint rule입니다.
// eslint-rules/require-named-effect.ts import { ESLintUtils } from "@typescript-eslint/utils"; const createRule = ESLintUtils.RuleCreator( (name) => `https://your-docs.dev/rules/${name}` ); export default createRule({ name: "require-named-effect", meta: { type: "suggestion", docs: { description: "Require named function expressions in useEffect for better stack traces", }, messages: { requireName: "useEffect의 callback에 이름을 붙여 주세요. " + "예: useEffect(function fetchData() { ... }, [])", }, schema: [], }, defaultOptions: [], create(context) { return { CallExpression(node) { if ( node.callee.type !== "Identifier" || node.callee.name !== "useEffect" ) { return; } const callback = node.arguments[0]; if (!callback) return; // 화살표 함수는 항상 경고 if (callback.type === "ArrowFunctionExpression") { context.report({ node: callback, messageId: "requireName" }); return; } // 이름 없는 function expression도 경고 if ( callback.type === "FunctionExpression" && !callback.id ) { context.report({ node: callback, messageId: "requireName" }); } }, }; }, });
이 rule을 ESLint 설정에 추가하면, 팀원이 useEffect(() => { ... }) 형태로 작성할 때 경고가 표시됩니다.
notion image
.eslintrc 설정 예시입니다.
{ "rules": { "custom/require-named-effect": "warn" } }
"warn"으로 시작하여 팀이 패턴에 적응한 뒤 "error"로 전환하는 것을 권장합니다. 기존 코드베이스에 대해서는 eslint-disable 주석으로 점진적으로 적용할 수 있습니다.

이 글에서는 useEffect에 named function expression을 적용하여 코드 가독성과 디버깅 효율을 동시에 높이는 방법을 다뤘습니다. 하지만 한 가지 짚고 넘어갈 점이 있습니다. useEffect에 이름을 잘 붙이는 것보다 더 중요한 것은, useEffect를 꼭 필요할 때만 사용하는 것입니다.
useEffect는 React component를 외부 시스템과 동기화하기 위한 탈출구(escape hatch)입니다. 데이터 fetching, 이벤트 구독, DOM 조작 등 외부 시스템과의 상호작용이 아닌 로직은 대부분 useEffect 없이 해결할 수 있습니다. effect의 이름을 고민하기 전에, 그 effect가 정말 필요한지를 먼저 질문해야 합니다. effect가 줄어들면 이름을 붙일 일도, 디버깅할 일도 함께 줄어듭니다.
useEffect의 올바른 사용 범위와 흔한 안티패턴에 대해서는 useEffect 범벅에서 벗어나기: 진짜 필요한 곳에만 쓰는 법에서 더 자세히 다루고 있습니다.

이 글에서는 useEffect에 named function expression을 적용하여 코드 가독성과 디버깅 효율을 동시에 높이는 방법을 다뤘습니다. 작은 변화지만, effect가 늘어날수록 그리고 production 에러를 추적할수록 체감할 수 있는 차이를 만듭니다. 팀 컨벤션으로 정착시키고 싶다면, 위에서 소개한 ESLint custom rule을 도입하는 것부터 시작해 보시기 바랍니다.