화면 깜빡임은 겉보기엔 하나지만 원인이 완전히 다른 두 가지가 섞여 있습니다. 어느 쪽인지부터 구분하면 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에게 그대로 전달하세요. 에러 첫 줄이 보통 원인 파일과 줄 번호를 가리킵니다.