React와 shadcn/ui를 사용해서 다크모드를 적용해보려고 합니다.
shadcn/ui를 선택한 이유는 공식 문서에 나온 대로 간단한 설치만으로 필요한 CSS와 컴포넌트 구조가 자동으로 설정되기 때문입니다.
이를 통해 별도의 스타일링 작업 없이도 바로 사용 가능하며 TailwindCSS와 자연스럽게 결합되어 있어 커스터마이징도 용이하기 때문에 사용했습니다.
언어 : TypeScript (v5.7.2)
라이브러리 : React (v19.0.0)
스타일 : TailwindCSS (v4.0.15)
빌드 도구: Vite (v6.2.0)
저는 base color를 Neutral 사용했습니다.
공식 홈페이지에서 Vite를 사용해서 다크모드 적용을 했습니다.
다른 부분을 작성하자면 storageKey를 webui-theme로 진행했습니다.
로컬스토리지에 저장되는 키 값이기 때문에 원하는 값으로 작성하시면 됩니다.
// ThemeProvider.tsx
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "webui-theme", ...props }: ThemeProviderProps) {
...
}
// App.tsx
export default function App() {
...
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="dark" storageKey="webui-theme">
<RouterProvider router={router} />
</ThemeProvider>
</QueryClientProvider>
)
}


워낙 짧은 프레임이어서 녹화에는 잘 보이진 않지만 다크모드로 적용 후 새로고침을 할 때마다 화면 깜빡임 현상이 발생합니다.
ThemeProvider로 적용하면 웹 페이지는 아래와 같이 html 태그에 class="dark"가 적용됩니다.
React에서 새로고침할 때 처음에는 라이트 모드(하얀 화면)로 보였다가 이후 다크 모드로 변경되는 현상은 아래의 과정 중에서 발생합니다.
DOM 트리 생성
<html> 태그가 먼저 렌더링되지만 class="dark"가 아직 적용되지 않은 상태스타일 규칙 생성
dark 모드가 localStorage에서 불러오기 전까지 기본 값(예: light)이 적용렌더 트리 생성 (③) → 레이아웃 (④)
useEffect 실행 → 다크 모드 적용
ThemeProvider.tsx에서 useEffect로 localStorage에서 저장된 테마 값을 읽고 class="dark"를 <html> 태그에 추가합니다.페인트(리페인트) (⑤) → 깜빡임 발생
class="dark"가 적용되면서 스타일이 변경됩니다.리페인트(Repaint) 과정이 발생하며, 이때 화면이 잠깐 깜빡임결론
깜빡임 현상은 초기 렌더링에서 다크 모드 스타일이 적용되지 않은 상태로 DOM이 생성된 후 React에서 상태를 변경하면서 리페인트가 발생하기 때문이를 방지하려면, HTML이 처음 로드될 때부터 class="dark"를 적용하도록
index.html에서 테마를 먼저 설정해야 합니다.
// index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ollama WebUi</title>
<script type="text/javascript">
(function () {
const isClient = typeof window !== "undefined";
if (isClient) {
const storageTheme = localStorage.getItem("webui-theme");
let theme = "light";
if (storageTheme === "system") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
} else if (storageTheme) {
theme = storageTheme;
}
document.documentElement.classList.add(theme);
document.documentElement.style.backgroundColor = theme === "dark" ? "#0a0a0a" : "#FFFFFF";
}
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
React가 실행되기 전에 index.html의 <head>에서 스크립트를 추가하여 초기 테마를 설정했습니다.
localStorage에서 저장된 테마 값을 확인하여 html 태그에 class="dark" 또는 class="light"를 즉시 추가
시스템 테마(prefers-color-scheme: dark)를 따르는 경우 이를 반영하여 테마를 결정
document.documentElement.style.backgroundColor를 설정하여 초기 렌더링 시 배경색이 올바르게 적용

적용 후 새로고침할 때마다 렌더링 전 theme가 미리 적용되어 깜빡임 현상을 해결할 수 있었습니다.