color palette 선정 및 light / dark 모드 구현

2023-10-22
5min
SCSSNext.js

color palette 선정

블로그에서 사용할 color palette 는 radix color 를 사용합니다. radix color 는 아래의 장점을 가지고 있습니다.

  • 하나의 색에 lightdark 를 모두 지원해줍니다.
  • 일반 css 파일로 제공됩니다.
  • color scale 세분화되어 있습니다.
  • 문서가 잘 되어있습니다.
    • color palette 에 대한 이해도 상승에 매우 도움이 됩니다.
    • 특정 color 와 잘 어울리는 gray color 를 찾을 수 있습니다.
    • 세분화 된 color scale 의 전반적인 사용 방법에 대해 말해줍니다. 따라서 프로젝트가 커져도 해당 규칙만 잘 지킨다면 프로젝트의 색 조화를 깨뜨릴 위험이 없습니다.

아래의 3가지 색상을 블로그의 primary / secondary / tertiary 로 선정했습니다.

  • primary: cyan
  • secondary: crimpson
  • tertiary: yellow
  • gray: slate

color palette 코드 구현

_light.scss
scss
// import ... :root { @include css-variable.colors; } @mixin light { @include slate; // gray @include cyan; // primary @include crimson; // secondary @include yellow; // tertiary @include blue; // ... } // NOTE: 추후 light / dark mode 를 위해 생성 @media (prefers-color-scheme: light) { :root { @include light; } } // NOTE: 추후 light / dark mode 를 위해 생성합니다. .light-theme { @include light; }
light 색을 지정합니다.
_light.scss
scss
// import ... :root { @include css-variable.colors; } @mixin light { @include slate; // gray @include cyan; // primary @include crimson; // secondary @include yellow; // tertiary @include blue; // ... } // NOTE: 추후 light / dark mode 를 위해 생성 @media (prefers-color-scheme: light) { :root { @include light; } } // NOTE: 추후 light / dark mode 를 위해 생성합니다. .light-theme { @include light; }
light 색을 지정합니다.
_dark.scss
scss
// import ... @mixin dark { @include slateDark; @include cyanDark; @include crimsonDark; @include yellowDark; // ... } @media (prefers-color-scheme: dark) { :root { @include dark; } } .dark-theme { @include dark; }
dark 색을 지정합니다.
_dark.scss
scss
// import ... @mixin dark { @include slateDark; @include cyanDark; @include crimsonDark; @include yellowDark; // ... } @media (prefers-color-scheme: dark) { :root { @include dark; } } .dark-theme { @include dark; }
dark 색을 지정합니다.

마지막으로 forwarding 해주면 됩니다.

_index.scss
scss
@forward "dark"; @forward "light"; @forward "css-variable"; @forward "utils";
forwarding 해줍니다
_index.scss
scss
@forward "dark"; @forward "light"; @forward "css-variable"; @forward "utils";
forwarding 해줍니다

이렇게 radix-colorglobal scss 로 관리되도록 세팅되었습니다. 생략되지 않은 모든 코드는 여기서 확인 할 수 있습니다.

color 를 bespoke 시스템으로 관리하자

해당 블로그 프로젝트는 불특정 다수가 사용할 수 있도록 만드는게 목표다. 따라서 위에서 지정한 primary 컬러 등을 각 사용자가 원하는 색으로 지정할 수 있도록 하고 싶었다. 가장 쉬운 방법으로 build 단계에서 원하는 색을 넣으면, 해당 값을 기준으로 scss 파일을 생성하도록 만들었다.

createColor.js
js
/* eslint-disable */ const fs = require("fs").promises const path = require("path") const defaultColors = { primary: "cyan", secondary: "crimson", tertiary: "yellow", gray: "slate", } async function createColor(colors) { const darkThemeFilePath = path.resolve() + "/src/styles/2-theme/_dark.scss" const lightThemeFilePath = path.resolve() + "/src/styles/2-theme/_light.scss" await fs.writeFile( lightThemeFilePath, getLightTemplate({ ...defaultColors, ...(colors || {}) }), {} ) await fs.writeFile( darkThemeFilePath, getDarkTemplate({ ...defaultColors, ...(colors || {}) }), {} ) }
createColor.js
js
/* eslint-disable */ const fs = require("fs").promises const path = require("path") const defaultColors = { primary: "cyan", secondary: "crimson", tertiary: "yellow", gray: "slate", } async function createColor(colors) { const darkThemeFilePath = path.resolve() + "/src/styles/2-theme/_dark.scss" const lightThemeFilePath = path.resolve() + "/src/styles/2-theme/_light.scss" await fs.writeFile( lightThemeFilePath, getLightTemplate({ ...defaultColors, ...(colors || {}) }), {} ) await fs.writeFile( darkThemeFilePath, getDarkTemplate({ ...defaultColors, ...(colors || {}) }), {} ) }
createBespoke.js
js
// import dox from "dox" /* eslint-disable */ const createColor = require("./createColor.js"); const createComponents = require("./createComponents.js"); module.exports = (pluginOptions = {}) => async (nextConfig = {}) => { await createColor(pluginOptions?.colors); await createComponents(); return Object.assign({}, nextConfig, { webpack(config, options) { if (typeof nextConfig.webpack === "function") { return nextConfig.webpack(config, options); } return config; }, }); };
createBespoke.js
js
// import dox from "dox" /* eslint-disable */ const createColor = require("./createColor.js"); const createComponents = require("./createComponents.js"); module.exports = (pluginOptions = {}) => async (nextConfig = {}) => { await createColor(pluginOptions?.colors); await createComponents(); return Object.assign({}, nextConfig, { webpack(config, options) { if (typeof nextConfig.webpack === "function") { return nextConfig.webpack(config, options); } return config; }, }); };
next.config.mjs
js
// 생략 ... const withBespoke = createBespoke({ colors: { primary: "cyan", secondary: "crimson", tertiary: "yellow", gray: "slate", }, }) export default withBespoke(withMDX(nextConfig))
next.config.mjs
js
// 생략 ... const withBespoke = createBespoke({ colors: { primary: "cyan", secondary: "crimson", tertiary: "yellow", gray: "slate", }, }) export default withBespoke(withMDX(nextConfig))

light / dark mode 구현

구체적인 코드는 코드 저장소에서 확인하시면 됩니다.

theme 을 구현하기 위해서는 아래의 것들을 고려해야합니다.

  1. 사용자 기기의 default theme 을 알아야합니다.
  2. 사용자가 이전에 페이지에 설정해 놓은 theme 이 무엇인지 알 수 있어야합니다.
  3. 페이지 접근 시 깜빡거림 없는 화면을 보여줘야합니다. (dark 로 지정해놨는데, 페이지 접근 시 흰화면이 보이고 dark 가 적용되면 안된다.)
  4. 적용된 theme상태client-side 에서 관리 되어야합니다.

위 고려사항들을 구현하기 위해서 아래 방법을 이용했습니다.

  1. matchMedia apiprefer-color-scheme 을 이용해서 사용자의 prefer color scheme 을 알아옵니다.
  2. color-scheme 을 저장하는 방법으로는 cookielocalstorage 가 있습니다. cookie 는 기본적으로 모든 서버요청에 header 로 포함되어 날라가게 됩니다. 굳이 서버로의 요청을 무겁헤 할 필요는 없으니, localstorage 를 사용합나디
  3. 페이지 접근 시 깜빡임 없는 화면 을 보여주기 위해서는 서버로부터 불러온 html 이 렌더링 되기 전에 client 영역에서 지정된 theme 을 확인하고, 곧바로 body 태그에 class를 지정해줍니다.
    • bespoke 블로그는 light / dark 를 class 명을 이용해 나타내게 됩니다. 만약 darkbody 의 class명에 dark-theme이 세팅됩니다.
    • 자세한 코드는 여기를 보시면 됩니다.
  4. theme 의 상태 관리는 jotai 를 이용해봤습니다. theme 이 변경될 때 마다 상태 변경은 물론, localStoragebodyclass명을 변경하도록 했습니다.