Next.js Tooltip 컴포넌트에서 useEffect + setState ESLint 에러 해결

오젼·2026년 3월 4일

React로 Tooltip 컴포넌트를 구현하던 중 다음과 같은 ESLint 에러가 발생했다.

react-hooks/set-state-in-effect
Avoid calling setState() directly within an effect

코드를 보면 단순히 useEffect에서 setState를 호출하고 있을 뿐인데 왜 이런 에러가 발생했는지, 그리고 왜 기존의 mounted 패턴이 필요 없게 되었는지 정리해보았다.


문제 상황

Tooltip 컴포넌트는 createPortal을 사용해서 Tooltip UI를 document.body에 렌더링하고 있었다.

const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true);
}, []);

그리고 Tooltip 렌더링 시 다음과 같은 조건을 두고 있었다.

if (!mounted || !isVisible) return null;

하지만 ESLint에서는 다음과 같은 에러가 발생했다.

react-hooks/set-state-in-effect
Avoid calling setState() directly within an effect

해당 에러는 React 렌더링 구조와 Next.js의 SSR 구조와 관련된 문제였다.


mounted 패턴이란?

위 코드는 흔히 mounted 패턴이라고 불린다.

const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true);
}, []);

if (!mounted) return null;

이 패턴의 목적은 컴포넌트가 브라우저에 마운트된 이후에만 UI를 렌더링하도록 만드는 것이다.

실제 렌더 흐름은 다음과 같다.

1. 첫 번째 render → mounted = false
2. useEffect 실행
3. setMounted(true)
4. 두 번째 render → mounted = true
render
↓
useEffect
↓
setState
↓
render

즉, 의도적으로 렌더를 한 번 더 발생시키는 패턴이다.


왜 이런 패턴이 필요했을까?

이 패턴은 주로 SSR(Server Side Rendering) 환경에서 사용되었다.

SSR에서는 다음과 같은 특징이 있다.

Server 환경
window ❌
document ❌
DOM ❌

즉 서버에서는 브라우저 객체가 존재하지 않는다.

따라서 이런 코드가 있으면 문제가 발생한다.

createPortal(..., document.body)

서버에서 document가 존재하지 않기 때문이다.

그래서 과거에는 다음과 같은 방식으로 해결했다.

Server render → 아무것도 렌더하지 않음
Client mount → 그 이후 렌더

즉 mounted 패턴을 통해 브라우저 환경에서만 렌더링하도록 만든 것이다.


그런데 Next.js use client 환경에서는?

이번 Tooltip 컴포넌트에는 다음 코드가 이미 존재했다.

"use client";

Next.js(App Router)에서 "use client"는 해당 컴포넌트를 Client Component로 만든다.

Client Component의 특징은 다음과 같다.

브라우저에서 실행
window 접근 가능
document 접근 가능
DOM 접근 가능

즉 Tooltip 컴포넌트는 이미 브라우저 환경에서만 실행되는 컴포넌트였다.

따라서 다음과 같은 문제는 발생하지 않는다.

document.body 접근 에러
SSR 환경 문제

결국 mounted 패턴은 필요 없는 코드가 된다.


해결 방법

mounted 상태를 제거하고 필요한 경우에만 렌더하도록 수정한다.

기존 코드

if (!mounted || !isVisible) return null;

수정 코드

if (!isVisible) return null;
if (typeof document === "undefined") return null;

return createPortal(..., document.body);

이렇게 하면

  • 불필요한 렌더 제거
  • ESLint 규칙 준수
  • Portal 렌더 안전성 확보

세 가지를 모두 만족할 수 있다.


정리

개념설명
mounted 패턴브라우저 mount 이후 렌더하기 위한 패턴
사용 이유SSR 환경에서 document 접근 문제 방지
문제점불필요한 추가 렌더 발생
Next.js "use client"브라우저 환경에서만 실행되는 컴포넌트
결론Client Component에서는 mounted 패턴이 대부분 필요 없음

핵심 요약

mounted 패턴은 SSR 환경에서 브라우저 API 접근 문제를 해결하기 위한 패턴이지만,
Next.js의 "use client" 컴포넌트에서는 이미 브라우저에서 실행되므로 대부분 필요하지 않다.

0개의 댓글