화면 깜빡임은 겉보기엔 하나지만 원인이 완전히 다른 두 가지가 섞여 있습니다. 어느 쪽인지부터 구분하면 90%는 해결됩니다.

1단계: 어떤 깜빡임인지 먼저 구분하세요

  • A. 처음 켤 때 딱 한 번 반짝: 페이지가 뜨는 순간 흰 화면이 보였다가 다크모드로 바뀌거나, 로그인 안 된 화면이 잠깐 보였다가 진짜 화면으로 바뀝니다. → 대부분 하이드레이션 미스매치 또는 테마 플래시.
  • B. 멈추지 않고 계속 깜빡깜빡: 화면이 계속 떨리거나 무한 새로고침되는 느낌이고, 탭이 느려지며 노트북 팬이 돕니다. → 대부분 무한 리렌더 루프.

확실히 구분하려면 브라우저에서 F12(개발자 도구) → Console 탭을 열어 빨간 에러를 보세요.

  • Hydration failed / Text content did not matchA
  • Maximum update depth exceededB

---

B. 계속 깜빡인다: 무한 리렌더 루프 (가장 흔함)

콘솔에 Maximum update depth exceeded가 뜨면 확정입니다. 원인은 거의 항상 하나입니다 — useEffect 안에서 상태(state)를 바꾸는데, 그 바뀐 상태가 다시 useEffect를 실행시켜 무한 반복됩니다.

AI(커서·v0·볼트 등)에게 이렇게 요청하세요 (복붙)

> Console에 Maximum update depth exceeded 에러가 나면서 화면이 계속 깜빡여요. useEffect 안에서 setState를 호출하는데 의존성 배열(dependency array)이 잘못돼서 무한 루프가 도는 것 같아요. 어떤 useEffect가 원인인지 찾아서, 의존성 배열을 고치거나 업데이터 함수(setCount(c => c + 1)) 형태로 바꿔주세요. 임시로 감추지 말고 근본 원인을 잡아주세요.

직접 코드를 볼 수 있다면 (React 공식 권장 수정 2가지)

1) 이전 값 기반 업데이트로 바꾸기 — 의존성 제거

js

// 문제: count가 바뀔 때마다 effect가 다시 돌아 무한 루프
useEffect(() => {
  setCount(count + 1);
}, [count]);

// 해결: 업데이터 함수 사용 + 의존성 비우기
useEffect(() => {
  setCount(c => c + 1);
}, []);

2) 객체·함수를 의존성으로 쓰지 말고 effect 안에서 만들기

js

// 문제: options 객체가 매 렌더마다 새로 만들어져 무한 루프
const options = { serverUrl, roomId };
useEffect(() => {
  createConnection(options);
}, [options]);

// 해결: effect 안에서 만들기
useEffect(() => {
  const options = { serverUrl, roomId };
  createConnection(options);
}, [roomId, serverUrl]);

핵심 체크: useEffect(() => { ... })처럼 의존성 배열 []을 아예 안 붙인 곳을 먼저 의심하세요. 배열이 없으면 매 렌더마다 실행되어 루프가 되기 쉽습니다.

---

A. 처음 한 번만 반짝인다: 하이드레이션 미스매치 / 테마 플래시

Next.js처럼 서버에서 미리 그린 화면(HTML)과 브라우저가 처음 그린 화면이 다르면, 그 순간 화면이 한 번 튑니다. 콘솔에 Text content did not match / Hydration failed가 뜹니다.

흔한 원인 (Next.js 공식 문서 기준)

  • window, localStorage, Date() 같은 걸 화면 그리는 코드에서 바로 사용
  • typeof window !== 'undefined' 같은 조건으로 서버와 화면을 다르게 그림
  • 다크/라이트 테마를 브라우저에서만 읽어 처음엔 반대로 보임
  • 브라우저 확장(번역기·다크모드 확장 등)이 HTML을 건드림 → 시크릿 창에서 테스트해 확인

해결 1: 브라우저에서만 필요한 부분은 useEffect로 미루기

서버/클라이언트가 처음엔 똑같이 그리고, 그 다음에 바꾸게 하는 방식입니다.

jsx

import { useState, useEffect } from 'react'

export default function App() {
  const [isClient, setIsClient] = useState(false)
  useEffect(() => { setIsClient(true) }, [])
  return <h1>{isClient ? '브라우저 전용 내용' : '미리 그린 내용'}</h1>
}

해결 2: 이 컴포넌트만 서버 렌더링 끄기 (지도·차트 등 브라우저 전용 위젯)

jsx

import dynamic from 'next/dynamic'
const NoSSR = dynamic(() => import('../components/no-ssr'), { ssr: false })

해결 3: 시계·날짜처럼 어쩔 수 없이 다른 값은 경고만 끄기

jsx

<time dateTime="2016-10-25" suppressHydrationWarning />

주의: 이건 한 단계만 적용되는 탈출구입니다. React가 불일치한 텍스트를 고쳐주지도 않으니 남용하지 마세요.

다크모드 깜빡임(흰 화면 → 어두워짐)이라면

next-themes를 쓰는 경우, 최상위 <html> 태그에 suppressHydrationWarning을 넣어야 하고, 테마 값을 읽어서 뭔가 그릴 땐 마운트된 뒤에 그려야 합니다.

jsx

// layout.jsx (app 디렉터리)
<html lang="ko" suppressHydrationWarning>

// 테마에 따라 다르게 그리는 컴포넌트
const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null

next-themes는 페이지가 뜨기 전에 실행되는 스크립트를 넣어 원래는 깜빡임이 없어야 정상입니다. 그래도 깜빡이면 위 두 가지(suppressHydrationWarning 누락, 마운트 전 렌더)가 빠졌을 확률이 큽니다.

---

AI에게 맡길 때 붙여넣을 만능 문장

> 화면이 [처음 한 번만 반짝 / 계속 깜빡]입니다. 개발자도구 Console에 나온 에러는 [에러 메시지 그대로 붙여넣기]입니다. 이 에러의 원인 컴포넌트를 찾아 공식 권장 방식으로 고쳐주세요. 임시로 감추지 말고 근본 원인을 잡아주세요.

확정 해결이 안 될 때 (지금 할 수 있는 것)

  • 브라우저 확장(번역·다크모드 확장 등)을 끄고 시크릿 창에서 재현되는지 확인 — 재현 안 되면 확장이 범인입니다.
  • Console 에러 전체를 복사해 AI에게 그대로 전달하세요. 에러 첫 줄이 보통 원인 파일과 줄 번호를 가리킵니다.