Skip to content

✨ [FEAT/#98] 테마 깜빡임 방지 스크립트 추가 - #99

Merged
yeonjin719 merged 26 commits into
developfrom
bugfix/#98
Jul 1, 2026
Merged

✨ [FEAT/#98] 테마 깜빡임 방지 스크립트 추가#99
yeonjin719 merged 26 commits into
developfrom
bugfix/#98

Conversation

@yeonjin719

@yeonjin719 yeonjin719 commented Jun 28, 2026

Copy link
Copy Markdown
Member

🚀 Related Issue

Closes #98

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Performance improvement

📝 Description

SSR/CSR 환경에서 저장된 테마가 React 초기화 이후에 적용되며 화면이 기본 테마로 잠깐 보이는 문제를 줄이기 위해 ThemeScript를 추가했습니다.

기존에는 ThemeProvider가 마운트된 뒤 useEffect에서 localStorage의 저장 테마를 읽고 <html data-theme="...">를 설정했습니다. 이 방식은 React 실행 이후에 테마가 적용되기 때문에, Next.js/Remix 같은 SSR 환경뿐 아니라 일반 CSR 환경에서도 첫 화면이 기본 테마로 잠깐 보일 수 있었습니다.

이번 PR에서는 다음을 변경했습니다.

  • ThemeScript 컴포넌트 추가

    • 앱의 <head>에 배치할 수 있는 inline script 컴포넌트입니다.
    • 브라우저 첫 페인트 전에 localStoragecolbrush-theme 값을 읽습니다.
    • 저장된 값이 default, protanopia, deuteranopia, tritanopia 중 하나일 때만 <html data-theme="...">를 설정합니다.
    • 잘못된 저장값이 있거나 localStorage 접근이 실패해도 앱이 깨지지 않도록 try/catch로 감쌌습니다.
  • ThemeScript export 추가

    • colbrush/client에서 ThemeScript를 import할 수 있도록 export했습니다.
    • 사용 예:
      import { ThemeProvider, ThemeScript } from 'colbrush/client';
  • ThemeProvider 초기 동기화 로직 개선

    • 기존 useEffect 기반 초기 동기화를 useSafeLayoutEffect로 변경했습니다.
    • 클라이언트에서는 useLayoutEffect를 사용해 브라우저 페인트 전에 React 상태와 DOM 테마를 최대한 빠르게 맞춥니다.
    • 서버 환경에서는 useEffect로 fallback하여 SSR 경고를 피합니다.
    • useState(() => localStorage...) 방식은 사용하지 않았습니다. SSR에서 서버 렌더 결과와 클라이언트 첫 렌더 결과가 달라져 hydration mismatch를 만들 수 있기 때문입니다.
  • 런타임 순환 import 방지

    • src/core/constants/modes.ts의 타입 import를 import type으로 변경했습니다.
    • ThemeScriptTHEME_MODES를 재사용하면서 불필요한 런타임 import가 생기지 않도록 정리했습니다.
  • README 업데이트

    • ThemeScript<head>에 추가해 theme flash를 방지하는 사용 예시를 문서화했습니다.
    • ThemeProvider는 앱 시작 이후 React 상태 동기화를 담당하고, 첫 페인트 전 테마 적용은 ThemeScript가 담당한다는 흐름을 안내했습니다.

사용 예시는 다음과 같습니다.

import { ThemeProvider, ThemeScript } from 'colbrush/client';
export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <ThemeScript />
      </head>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

✔️ Checklist

  • I've read the Contributing Guide
  • I have checked that I'm merging into the correct branch (not master by mistake)
  • I have tested these changes locally and they work without errors
  • I have run pnpm build and it completes successfully
  • I have run pnpm lint and there are no linting errors
  • I have added/updated tests if needed
  • I have updated documentation if needed

📸 Screenshots (Optional)

해당 변경은 초기 테마 적용 타이밍 개선 및 public API 추가 작업이라 별도 UI 스크린샷은 없습니다.

💬 Review Notes (Optional)

리뷰 시 아래 부분을 중점적으로 확인 부탁드립니다.

  • ThemeScript의 inline script가 허용된 테마 값만 에 적용하는지
  • ThemeProvider에서 useSafeLayoutEffect를 사용하는 방식이 SSR 환경에서 경고 없이 동작하는지
  • useState(() => localStorage...)를 피한 판단이 hydration mismatch 방지 관점에서 적절한지
  • README의 사용 예시가 Next.js/Remix/CSR 사용자에게 충분히 명확한지
  • 현재 매우 많은 코드 변경이 있다고 뜨는데 develop이 업데이트 되지 않아서 rebase로 인한 코드 변경입니다.. 5a21791, 857fc27 커밋 내역만 확인해주시면 됩니다!
    검증 결과:
  • ESLint 통과
  • tsup 빌드 및 d.ts 생성 통과
  • 사이트 E2E 테스트 5개 통과

hyesngy and others added 24 commits October 31, 2025 09:17
✨ [Feature] 테스트 코드 추가
[Develop] update dependent
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
…e/vite-6.4.2

chore(deps-dev): bump vite from 6.4.1 to 6.4.2 in /e2e/site
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 5.0.6.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.12...v5.0.6)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…xpansion-5.0.6

chore(deps): bump brace-expansion from 1.1.12 to 5.0.6 (via audit fix)
@yeonjin719
yeonjin719 requested a review from Copilot June 28, 2026 13:13
@yeonjin719 yeonjin719 self-assigned this Jun 28, 2026
@yeonjin719
yeonjin719 requested a review from a team as a code owner June 28, 2026 13:13
@yeonjin719 yeonjin719 added bug Something isn't working feature New feature or improvement request labels Jun 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a pre-hydration theme initialization path to reduce “theme flash” (FOUC) by applying a persisted theme before the first paint, while keeping React state and DOM attributes synchronized after the app starts.

Changes:

  • Added ThemeScript (inline <script>) that reads localStorage early and sets <html data-theme="..."> for allowed themes.
  • Updated ThemeProvider to perform initial synchronization via a useSafeLayoutEffect (layout effect on client, effect on server) for earlier DOM alignment.
  • Exported ThemeScript from colbrush/client and documented recommended SSR/CSR usage in the README; adjusted type imports in modes.ts to avoid runtime cycles.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/react/ThemeScript.tsx New inline script component to apply persisted theme before first paint.
src/react/ThemeProvider.tsx Uses a “safe” layout effect for earlier initial DOM/state synchronization.
src/core/constants/modes.ts Converts imports to import type to prevent runtime circular import pressure.
src/client.ts Exposes ThemeScript via the public client entry.
README.md Documents how to place ThemeScript in <head> to prevent theme flash.
Comments suppressed due to low confidence (1)

src/react/ThemeProvider.tsx:78

  • The initial theme/language/simulation sync reads from localStorage without a try/catch. In some browser/privacy contexts, accessing localStorage can throw (e.g. storage blocked), which would crash the provider during layout effect and prevent rendering. Consider guarding the whole sync block and falling back to defaults when storage is unavailable.
    useSafeLayoutEffect(() => {
        if (typeof window === 'undefined') return;
        const storedTheme = normalizeToKey(
            localStorage.getItem(ThemeStorageKey)
        );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/react/ThemeScript.tsx
@yeonjin719
yeonjin719 changed the base branch from master to develop June 28, 2026 13:23
@yeonjin719
yeonjin719 merged commit 6c9c6cd into develop Jul 1, 2026
@yeonjin719
yeonjin719 deleted the bugfix/#98 branch July 1, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working feature New feature or improvement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 [BugFix] SSR 환경에서 저장된 테마 적용 시 초기 렌더 FOUC 발생

3 participants