테크

2024. 07. 11

에브리타임(WEB) 다크모드 개발 과정 톺아보기

에브리타임 웹 다크모드 개발팁

에브리타임(WEB) 다크모드 개발 과정 톺아보기에브리타임(WEB) 다크모드 개발 과정 톺아보기

최근 몇 년 동안 모바일 운영 체제, 웹 브라우저 및 애플리케이션에서는 다크 모드가 사용자 경험을 향상시키는 중요한 요소로 자리 잡고 있습니다. 다크 모드는 눈의 피로를 줄이고 배터리 수명을 연장하는 등 다양한 이점을 제공합니다. 이번 글에서는 웹에서 다크 모드를 구현하는 다양한 방법과 에브리타임 웹사이트에서 이를 적용한 과정을 알아보겠습니다.


다크 모드를 지원하는 가장 간단한 방법은 브라우저의 기본 사용자 스타일 시트인 user agent stylesheet(UA stylesheet)을 활용하는 것입니다. 이 스타일 시트는 브라우저에 따라 미리 정의된 스타일을 제공하며, meta 태그를 통해 다크 모드를 지원할 수 있습니다.


<head>
  <meta name="color-scheme" content="dark light">
  <title>다크 모드</title>
  <style>
    form {
      margin: 0 auto;
      max-width: 360px;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    form > input {
      box-sizing: border-box;
      padding: 10px;
    }
  </style>
</head>
<body>
  <form action="/login" method="post">
    <input name="id" type="text" placeholder="아이디">
    <input name="password" type="password" placeholder="비밀번호">
    <input type="submit" value="에브리타임 로그인">
  </form>
</body>




위의 에브리타임 로그인 화면은 별도의 색상을 지정하지 않고 크롬에서 UA stylesheet만 사용한 결과입니다. meta 태그를 통해 사용자의 테마 설정을 반영하도록 한 것이죠. 이 방법은 간단하지만, 모든 브라우저에서 일관된 스타일을 보장하지 않을 수 있습니다. 예를 들어, select 태그는 브라우저마다 디자인이 다르기 때문에 UA stylesheet에 의존하지 않고 reset이나 normalize 작업을 통해 브라우저 간 스타일 차이를 최소화하는 것이 권장됩니다. 즉, 실무에서는 직접 다크모드 스타일을 구현해야 하죠.



다크 모드 구현을 위한 주요 방식

1️⃣ CSS 미디어 쿼리 ‘prefers-color-scheme’

웹에서 다크 모드를 구현하는 방법은 비교적 간단합니다. prefers-color-scheme 을 사용하여 사용자의 시스템 테마 설정을 감지하고 이에 따라 스타일을 변경할 수 있습니다.


@media (prefers-color-scheme: light) {
  color: #F9F9F9;
}

@media (prefers-color-scheme: dark) {
  color: #1F1F1F;
}


그러나 미디어 쿼리만으로는 사용자가 웹사이트에서 직접 테마를 변경할 수 있는 기능을 제공할 수 없습니다. JavaScript를 사용하여 테마 설정을 관리하는 방식을 고려해야 합니다. 사용자가 토글 버튼을 통해 테마를 변경하면, 해당 값을 상태 관리하는 방식으로 구현해야 하죠.


// 다크 모드인 경우 true
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches


이후 토글 버튼에 로직을 추가하여 html 태그에 현재 설정된 테마를 클래스로 추가하면, 다음과 같은 스타일로 구현할 수 있습니다.


html {
  color: #F9F9F9;
}

html.dark {
  color: #1F1F1F
}


2️⃣ CSS 변수

CSS Variables를 활용하는 것도 유용한 방법입니다. 테마 색상을 변수로 정의하고, 미디어 쿼리나 JavaScript를 통해 변수 값을 변경하여 다크 모드를 구현합니다. 동일한 속성에 대해 색상 값을 변경할 수 있어 코드의 중복을 줄이고 유지보수성을 높일 수 있습니다.


/* 변수 선언 */
html {
  --color: #F9F9F9;
}

html.dark {
  --color: #1F1F1F
}

/* 테마에 맞게 설정된 변수로 설정 */
html {
  color: var(--color);
}


3️⃣ 서버 사이드 렌더링(SSR)

마지막으로, 웹에서 다크모드를 구현할 때 발생할 수 있는 화면 깜박임(flicker) 문제에 대해서도 고려해야 합니다. 화면 깜박임은 Next.js나 Nuxt.js와 같은 서버 사이드 렌더링(SSR)을 지원하는 프레임워크를 사용할 때 주로 발생합니다. 서버에서 html 태그에 사용자의 다크 모드 클래스를 포함하지 않기 때문에 클라이언트에서 클래스가 추가되기 전에 하얀 화면이 잠깐 보이는 것이죠.


화면 깜박임을 해결하기 위해서는 크게 두 가지 방법이 있습니다.


  1. SSR 시 다크 모드 정보 포함


    - 클라이언트가 요청할 때 다크 모드 정보를 서버로 전달하고, 서버에서 이를 HTML에 반영해서 응답합니다.

  2. 렌더링 차단 리소스 영역에서 스크립트 실행


    - 페이지 렌더링을 차단하는 리소스 영역에서 스크립트를 실행하여 초기 로딩 시점에 다크 모드 클래스를 추가합니다.




에브리타임에 다크 모드 구현하기(1):: 개발 스택

다크 모드를 구현하는 방법은 여러가지가 있지만, 다양한 요소들을 종합적으로 고려해 서비스에 적합한 방법을 선택하는 것이 중요합니다.


에브리타임의 경우에는 웹 페이지 뿐만 아니라, 앱 내에서 웹뷰로 동작하는 페이지가 있는데요. 사용자는 앱을 통해 테마를 설정할 수 있는 기능을 제공 받기 때문에 웹에서는 이 기능을 별도로 구현할 필요가 없습니다. 웹에서는 prefers-color-scheme을 통해 사용자 설정을 판단하기만 하면 됩니다.


또한, 에브리타임은 여전히 Internet Explorer(IE)를 지원 중이기 때문에, 이 레거시 브라우저에서도 다크 모드를 지원하는 코드를 구현해야 합니다. IE에서 CSS Variables을 지원하지 않기 때문에, 다음과 같이 fallback 코드를 추가해야 합니다.


color: #F9F9F9;
color: var(--color);


이외에도, 다크 모드 구현을 위한 색상 값 변경 코드를 최소화하고 직관적인 구조를 유지하는 것이 중요합니다. 개발 생산성을 유지하면서도 사용자 경험을 개선하는 데 집중할 수 있기 때문이죠.

에브리타임의 프론트엔드 개발 스택은 이러한 요구 사항을 충족하기 위해 여러 기술을 통합적으로 활용하고 있는데요. 간단히 소개해 보겠습니다.


☑️ SCSS

SCSS는 CSS 전처리기로, 가독성을 높이고 다양한 기능을 제공하는 동시에 CSS 문법을 확장하고 보완할 수 있습니다. 변수와 함수를 사용하여 코드를 단순화하고 관리할 수도 있는데요.


변수는 변수명이 $로 시작해야 한다는 규칙만 지키면 다른 언어들과 비슷합니다.


// 변수 선언
$color_gray100: #F9F9F9;

// 변수 값 적용
color: $color_gray100;


함수는 파라미터를 정의하고 값을 반환하는 형태로 사용됩니다. SCSS는 Map 자료구조도 지원하여, 변수의 다크 모드 값을 darken_map에 등록해 두면, darken 함수를 통해 다크 모드에 맞는 색상 값을 손쉽게 가져올 수 있습니다.


// map 자료구조를 지원합니다
$darken_map: (
  #{$color_gray100}: #1F1F1
)

// 함수 선언
@function darken($color) {
  @return map-get($darken_map, #{$color});
}

// 다크 모드 값 적용
color: darken($color_gray100);


위에서 정의한 변수와 함수를 사용하면, 라이트 모드와 다크 모드를 모두 지원할 수 있습니다.


color: $color_gray100;

@media (prefers-color-scheme: dark) {
  color: darken($color_gray100)
}


☑️ 웹팩(Webpack)

에브리타임에서는 모듈 번들러로 웹팩을 사용하고 있습니다. 웹팩은 JavaScript, CSS, 이미지 등 다양한 파일을 브라우저가 해석할 수 있도록 번들링하는 도구입니다. SCSS 파일 역시 웹팩을 통해 번들링 되며, 이 과정은 주로 로더(Loader)에 의해 처리됩니다. SCSS 파일의 경우, sass-loader와 css-loader가 이를 컴파일하고 번들링하는 역할을 합니다.


또한, 컴파일과 번들링 과정에서는 PostCSS도 사용됩니다. PostCSS는 CSS 코드를 추상 구문 트리(AST)로 변환하여 플러그인으로 각 노드를 쉽게 조작할 수 있게 합니다. 대표적인 플러그인으로는 브라우저 호환성을 위해 자동으로 접두사를 붙여주는 Autoprefixer가 있습니다.


이렇게 웹팩과 PostCSS를 함께 사용하면, 다양한 파일 형식을 효율적으로 처리하고, 브라우저 호환성을 높이는 데 도움이 됩니다.


rules: [
  ..., 
  {
    test: /\\.s?css$/,
    use: [
      "css-loader",
      {
        loader: "postcss-loader",
        options: {
          postcssOptions: {
            plugins: [autoprefixer]
          }
        }
      },
      "sass-loader"
    ]
  }
]



에브리타임에 다크 모드 구현하기(2):: 구현 방법

이번에는 에브리타임 웹사이트에 다크 모드를 지원하기 위해 SCSS와 PostCSS를 사용한 방법을 소개하겠습니다. 이 방법은 SCSS 함수와 PostCSS 플러그인을 사용하여 SCSS 문법으로 원하는 CSS를 생성해 다크 모드를 쉽게 구현할 수 있도록 돕습니다.


1. SCSS 함수 작성

먼저, SCSS 함수를 임의로 정한 포맷으로 코드를 변환합니다.


// 함수 선언
@function theme($light) {
  @return #{"theme__(__"($light"__"darken($light))"__)__"};
}

// 스타일 적용
color: theme($color_gray100);


2. SCSS 컴파일 결과

위 SCSS 코드를 컴파일하면, 아래와 같은 CSS 코드가 생성됩니다.


/* 컴파일된 CSS */
color: theme__(__#F9F9F9__#1F1F1F__)__;


이 코드는 실제 CSS 문법에 맞지 않지만, PostCSS 플러그인을 사용하여 올바른 CSS로 변환할 수 있습니다.


3. PostCSS 플러그인 작성

PostCSS 플러그인으로 미디어 쿼리를 생성합니다. PostCSS는 AST 노드를 탐색하면서 특정 유형의 노드를 만나면 해당 노드에 대한 함수를 호출합니다. 주요 노드 유형에는 최상위 노드(Root), 블록 단위 노드(Rule), 선언 노드(Declaration) 등이 있는데요. 이번에는 블록 단위로 미디어 쿼리를 생성하기 위해 Rule 함수를 사용했습니다.


import { PluginCreator, Rule } from 'postcss';

const creator: PluginCreator<never> = () => {
  return {
    postcssPlugin: "postcss-theme-plugin",
    Rule(rule: Rule) {
      // theme 포맷을 파싱하기 위한 정규식
      const regex = /theme__\\(__\\s?(.+)\\s?__\\s?(.+)\\s?__\\)__/g;
      // 다크 모드 declaration 저장할 배열
      const darkModeDecls: Declaration[] = [];

      // rule 내의 모든 declaration을 탐색하면서 theme 포맷이 있는지 검사합니다
      rule.walkDecls(decl => {
        if (!regex.test(decl.value)) {
          return;
        }
        const lightValue = decl.value.replace(regex, (_, lightColor, darkColor) => {
          return lightColor;
        });
        const darkValue = decl.value.replace(regex, (_, lightColor, darkColor) => {
          return darkColor;
        });
        // 현재 declaration 값을 라이트 모드 값으로 업데이트
        decl.value = lightValue;
        // 다크 모드 declaration 저장
        darkModeDecls.push(new Declaration({
	  prop: decl.prop,
	  value: darkValue
	}));
      });

      // 다크 모드 declaration이 있으면 미디어 쿼리로 추가
      if (darkModeDecls.length > 0) {
        const darkRule = new AtRule({
          name: 'media',
          params: '(prefers-color-scheme: dark)'
        });
        // 현재 rule을 복사하고 모든 declaration을 제거
        const newRule = rule.clone().removeAll();
        // 새로운 rule에 다크 모드 declaration을 추가
        newRule.append(darkModeDecls);
        // 미디어 쿼리에 새로운 rule을 추가
        darkRule.append(newRule);
        rule.after(darkRule);
      }
    }
  }
};


4. 최종 CSS 출력

이 플러그인을 PostCSS의 플러그인으로 추가하고 빌드하면 다음과 같은 CSS 파일로 컴파일됩니다.


color: #F9F9F9;

@media (prefers-color-scheme: dark) {
  color: #1F1F1F;
}


이와 같이 SCSS 함수와 PostCSS 플러그인을 통해 브라우저가 사용할 최종 CSS를 생성할 수 있습니다. 프론트엔드 개발자는 다크 모드를 적용해야 하는 경우, 색상 값을 theme 함수로 감싸기만 하면 됩니다.


앞서 설명한 방법을 사용하면 SCSS 함수와 PostCSS 플러그인을 통해 미디어 쿼리 외에도 다양한 기능을 추가할 수 있습니다. PostCSS 플러그인으로 직접 제어할 수 있기 때문에 더 많은 커스터마이징이 가능합니다. 예를 들어, theme 함수를 고도화하면 색상 뿐만 아니라 이미지 등의 다양한 값을 처리할 수 있고, 컴파일 타임에 에러도 감지할 수 있죠. 자신의 프로젝트에 맞게 필요한 PostCSS 플러그인을 선택하여 사용함으로써 생산성을 향상시키고, 유지보수를 간편하게 할 수 있습니다.


에브리타임에는 SCSS 함수와 PostCSS 플러그인을 선택하여 다크 모드를 구현했지만, 사실 정답은 없습니다. 서비스와 디자인의 요구에 따라 유연하게 변경될 수 있죠. 브라우저의 지원 범위, 사용하는 개발 스택 등 다양한 조건과 프로젝트의 특성에 맞춰 최적의 방법을 선택하고 적용해야 할 것입니다.


Written by 오주영