Nextjs 다양한 테마 적용하기 with SSR

김동균·2023년 1월 10일
4
post-thumbnail

테마 적용

  • 퍼즐 사이트에 다채로운 색을 주기 위하여 여러 가지 테마를 적용해보았다.
  • 테마 색상은 핑크, 민트, 실버, 다크다.
  • css 라이브러리는 styled-components를 사용하였다.

localStorage 테마 저장

  • 테마는 클라이언트에서 저장하고 있어야 한다. 따라서 localStorage에 저장하였다.

테마 적용은 4개의 파일로 구성된다.

  1. theme.ts: 테마에 따른 색상들을 담아 놓는다.
  2. ThemeProvider.tsx: styled-components에서 제공하는 컴포넌트를 만들어 테마를 적용시킨다.
  3. useTheme.ts: 테마를 변경할 수 있는 함수다. window.localStorage를 사용하여 테마를 localStorage에 저장 또는 가져온다.
  4. _app.tsx: ThemeProvider 컴포넌트로 공통 레이아웃을 감싼다.

code

// theme.ts
// 민트, 핑크 색상은 생략하였다.
const colors = {
  dark: 'rgb(43, 42, 43)', // #2b2a2b
  silver: 'rgb(233, 230, 228)', // #e9e6e4
  white: 'rgb(255, 255, 255)', // #ffffff
};

export const silverTheme: Theme = {
  bgColor: colors.silver,
  textColor: colors.dark,
};

export const darkTheme: Theme = {
  bgColor: colors.dark,
  textColor: colors.white,
};

export const theme = {
  darkTheme,
  silverTheme,
  colors,
};
// ThemeProvider.tsx
import { silverTheme, darkTheme, pinkTheme, mintTheme } from './theme';
import React, { useState, useMemo } from 'react';
import { ThemeContext, ThemeProvider as StyledProvider } from 'styled-components';

interface Props {
  children: React.ReactNode;
}

const THEME: {
  [key in ThemeKey]: Theme;
} = {
  pink: pinkTheme,
  dark: darkTheme,
  silver: silverTheme,
  mint: mintTheme,
};

const ThemeProvider = ({ children }: Props) => {
  const [themeMode, setThemeMode] = useState<ThemeKey>('pink');
  const themeObject = useMemo(() => {
    return THEME[themeMode];
  }, [themeMode]);

  return (
    <ThemeContext.Provider value={{ themeMode, setThemeMode }}>
      <StyledProvider theme={themeObject}>{children}</StyledProvider>
    </ThemeContext.Provider>
  );
};

export { ThemeProvider };
// useTheme.ts
import { useCallback, useContext, useEffect, useLayoutEffect, useMemo } from 'react';
import { ThemeContext } from 'styled-components';

function useTheme() {
  const themeKeys: ThemeKey[] = useMemo(() => ['pink', 'silver', 'mint', 'dark'], []);
  const context = useContext(ThemeContext);
  const { themeMode, setThemeMode } = context;
  const canUseDOM = typeof window !== 'undefined';
  const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;

  const setTheme = useCallback(
    (theme: ThemeKey) => {
      setThemeMode(theme);
      window.localStorage.setItem('localTheme', theme);
    },
    [setThemeMode]
  );

  useIsomorphicLayoutEffect(() => {
    const localTheme = window.localStorage.getItem('localTheme') as ThemeKey;
    if (themeKeys.includes(localTheme)) {
      setTheme(localTheme);
    }
  }, [setTheme, themeKeys]);

  return [themeMode, setTheme];
}
export default useTheme;
// pages/_app.tsx
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;

설명 및 문제점

  • useTheme에서 useLayoutEffect를 사용하여 렌더링 전에 테마를 localStorage에서 받아와 적용 시켜준다. 따라서 새로 고침 시 기본 테마가 깜빡거리는 현상은 발생하지 않는다.
  • useLayoutEffectuseIsomorphicLayoutEffect로 커스텀하여 SSR 적용에 따른 에러는 발생하지 않는다.
  • 하지만 문제점이 두 가지 발생한다.

문제점

  1. 페이지를 이동하여 처음 렌더링 될 때 기본 테마인 핑크 테마가 잠깐 보여진다.
  2. useTheme.ts 내부에서 localStorage에 접근하다 보니 setTheme를 사용하지 않는 페이지에는 테마 적용이 되지 않는다.

해결 방법

  1. _document.tsx에서 localStorage에 접근하여 테마 적용한다.
  2. 테마를 cookie에 저장 후, _app.tsxgetInitialProps에서 cookie에 접근하여 SSR 적용한다.
  3. SSR을 지원하는 Nextjs를 사용하니 두 번째 방법이 더 깔끔할 것 같아, 두 번째 방법을 채택하여 cookie에 저장하여 SSR 적용 시키기로 하였다.

테마 적용은 4개의 파일로 구성된다.

  1. theme.ts: 테마에 따른 색상들을 담아 놓는다.
  2. ThemeProvider.tsx: styled-components에서 제공하는 컴포넌트를 만들어 테마를 적용 시킨다. useState의 값을 테마로 사용한다. 해당 값의 초기 값은 cookie에 저장되어 있는 값이다.
  3. useTheme.ts: 테마를 변경할 수 있는 함수다. 테마 변경 시 cookie의 값과 useState의 값을 변경 시켜준다.
  4. _app.tsx: ThemeProvider 컴포넌트로 공통 레이아웃을 감싼다. getInitialPropscookie의 값을 받아와 ThemeProvider 컴포넌트로 전달한다.

code

// theme.ts
// 위 localStorage 테마 저장 방식과 동일
// ThemeProvider.tsx
const ThemeProvider = ({ children, pageTheme }: Props) => {
  const [themeMode, setThemeMode] = useState<ThemeKey>(pageTheme);
  const themeObject = useMemo(() => {
    return THEME[themeMode];
  }, [themeMode]);

  return (
    <ThemeContext.Provider value={{ themeMode, setThemeMode }}>
      <StyledProvider theme={themeObject}>{children}</StyledProvider>
    </ThemeContext.Provider>
  );
};

export { ThemeProvider };
// useTheme.ts
import { setCookie } from 'cookies-next';

function useTheme() {
  const context = useContext(ThemeContext);
  const { themeMode, setThemeMode } = context;

  const setTheme = useCallback(
    (theme: ThemeKey) => {
      setThemeMode(theme);
      setCookie('localTheme', theme);
    },
    [setThemeMode]
  );

  return [themeMode, setTheme];
}
export default useTheme;
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider pageTheme={pageProps.theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

MyApp.getInitialProps = async ({ ctx, Component }: {ctx: any; Component: any;}) => {
  const themeKeys: ThemeKey[] = ['pink', 'silver', 'mint', 'dark'];
  let pageProps: any = {};
  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  const cookie = ctx.req.cookies;
  if (cookie) {
    const localTheme = getCookie('localTheme', ctx) as ThemeKey;
    if (themeKeys.includes(localTheme)) {
      Object.assign(pageProps, {
        theme: localTheme,
      });
    } else {
      Object.assign(pageProps, {
        theme: 'pink',
      });
    }
  }

  return { pageProps };
};

export default MyApp;

결과

  1. useTheme.ts 내부에서만 localStorage에 접근하여 테마를 적용 시키는 방식이 _app.tsx에서 테마를 SSR 적용 시키는 방식으로 바껴서 모든 페이지에 코드 추가 없이 테마 적용이 되었다.
  2. cookie의 값을 가지고 테마에 SSR 적용하여 화면이 처음 렌더링 될 때 기본 테마가 잠깐 보이는 것이 사라졌다.
  3. 네트워크를 확인해 보면 테마가 SSR이 적용되는 것을 볼 수 있다.
    • mint theme
    • dark theme
profile
초보 개발자

0개의 댓글