[번역] 리액트 개발을 위한 CSS 변수

Minjeong Koo·2022년 12월 27일
21

좋은 아티클 번역

목록 보기
8/8
post-thumbnail

원문: CSS Variables for React Devs

논란의 여지가 있는 의견이지만 저는 CSS-in-JS를 선호합니다. 😬🚨

하지만! CSS도 정말 좋아합니다. 그리고 CSS-in-JS를 사용한다고 해서 CSS를 배울 필요가 없어진다고는 생각하지 않습니다. 어느 쪽이든 CSS를 사용하고 있습니다! 그저 조금 다르게 포장되어 있을 뿐이에요.

당신이 CSS를 어디에 두든, 언어를 숙련시키는 것은 반드시 해야 합니다. CSS를 더 잘하게 되면 당신은 더 뛰어난 프론트엔드 개발자가 될 수 있습니다.

이 튜토리얼에서는 CSS에서 가장 흥미로운 최신 개발 중 하나인 CSS 변수, AKA 사용자 지정 속성을 활용하는 방법을 살펴보겠습니다. React 앱에서 이들을 어떻게 사용하여 워크플로우를 개선하고 멋진 작업을 수행할 수 있는지 살펴보겠습니다.

그런데 왜 해야하죠?

리액트 개발자로서, 당신은 CSS에 변수가 필요하지 않다고 생각할 수 있습니다. 마음대로 사용할 수 있는 자바스크립트 엔진이 있으니까요!

하지만, React 앱에서 CSS 변수로 전환해야 하는 두 가지 이유가 있습니다.

  1. 인체 공학적(ergonomics)적으로 좋습니다.
  2. 새로운 가능성을 열어줍니다! JS로는 불가능한 것들을 CSS 변수로는 할 수 있습니다.

그것들을 사용하는 방법을 살펴보고, 어떤 일이 펼쳐지는지 알아보겠습니다!

빠르게 소개합니다

CSS 변수의 동작을 살펴보겠습니다:

이 예시에서는 문단 선택자에 새로운 변수인 --highlight-color를 정의하고 있고, 이 색상을 사용하여 하위 <em> 요소에 배경색을 적용하고 있습니다. 문단 내에서 <em>을 사용할 때마다 노란색 배경을 사용합니다.

일반적인 CSS 프로퍼티를 정의하는 것과 동일한 방식으로 CSS 변수를 정의하는 것에 주목해주세요. 이것은 CSS 변수프로퍼티이기 때문입니다. 이는 공식적으로 "CSS 사용자 정의 프로퍼티(CSS Custom Properties)"라고 불리는데, 그럴만한 이유가 있습니다!

CSS 변수는 font-size 또는 color와 마찬가지로 기본적으로 상속 가능합니다. 엘리먼트에 변수를 정의하면 해당 엘리먼트의 모든 자식에서 변수를 사용할 수 있습니다.

많은 개발자들은 CSS 변수가 전역 변수라고 믿고있지만 완전히 틀렸습니다. 아래를 살펴봐주세요.

이 헤딩 안의 embackgroundvar(--highlight-color)로 설정하지만 --highlight-color가 이 엘리먼트에 대해 정의되지 않았기 때문에 아무 효과가 없습니다. 문단에 대해서만 정의되었으며 이 <em> 태그는 문단 내에 없습니다.

우리는 때로 CSS 변수를 전역 변수처럼 사용하고 싶을 때가 있습니다. 예를 들어, CSS 변수가 색상 테마로 사용하는 경우에는 어플리케이션 전체에 CSS 색상 변수를 사용할 수 있기를 원합니다.

이를 위해, 최상위 요소인 <html>에 CSS 변수를 적용할 수 있습니다.

/* CSS */
/*
  이 변수는 어디서든 사용할 수 있습니다.
  모든 엘리먼트가 HTML 태그의 하위 엘리먼트이기 때문입니다.
*/
html {
  --color-red: hsl(0deg 80% 50%);
  --color-blue: hsl(270deg 75% 60%);
}

:root?

튜토리얼들에서는 종종, CSS 변수가 미스테리한 :root 선택자에 연결되는 것을 보여줍니다.

/* CSS */

:root {
  --color-red: hsl(0deg 80% 50%);
  --color-blue: hsl(270deg 75% 60%);
}

:root는 최상위 엘리먼트에 대한 참조입니다. html 선택자와 동일합니다. 간단히 하기 위해, 이 튜토리얼에서는 html 참조를 사용하겠습니다.


CSS 변수가 일반적인 CSS 속성과 다른 점은 var() 함수를 사용하여 해당 값에 액세스할 수 있다는 것입니다. 이것이 CSS 사용자 정의 속성을 변수로 사용할 수 있게 해줍니다.

CSS 변수에 대한 몇 가지 간단한 사실은 다음과 같습니다.

  • 사용자 지정 속성은 두 개의 대시로 시작해야 합니다. 이것이 전통적인 CSS 속성과 다른 점입니다.
  • 색상과 픽셀뿐만 아니라 모든 유형의 값을 저장할 수 있습니다.
  • CSS 변수가 정의되지 않은 경우 기본값을 지정할 수 있습니다. var(--primary-color, pink)는 필요한 경우 pink색으로 대체됩니다.

리액트 앱에서

리액트 앱에서는 어떻게 보이는지 살펴보겠습니다. 이 튜토리얼은 styled-components를 사용하지만 설명은 CSS-in-JS 라이브러리에 관계없이 비교적 유샤해야 합니다.

먼저, 다음과 같이 모든 디자인 토큰을 저장하는 파일이 있다고 가정하겠습니다.

// JS
const COLORS = {
  text: 'black',
  background: 'white',
  primary: 'rebeccapurple',
};

const SIZES = [
  8, 
  16, 
  24, 
  32,
  /* 등등 */
];

React 앱에서 필요한 컴포넌트로 직접 가져올 수 있습니다.

//JSX

import { COLORS } from '../constants';
const Button = styled.button`
  background: ${COLORS.primary};
`;

또는 다음과 같은 테마를 사용할 수 있습니다.

// components/App.js
import { ThemeProvider } from 'styled-components';

import { COLORS } from '../constants';

// 이 엘리먼트는 컨텍스트를 통해 테마를 사용할 수 있도록 전체 어플리케이션을 래핑합니다.
const App = ({ children }) => {
  return (
      <ThemeProvider theme={{ colors: COLORS }}>
        {children}
      </ThemeProvider>
  );
};

// 다른 곳에서는…
const Button = styled.button`
  background: ${(props) => props.theme.colors.primary};
`;

다음은 동일한 코드이지만 CSS 변수를 사용하여 설정합니다.

import { createGlobalStyle } from 'styled-components';

const GlobalStyles = createGlobalStyle`
  html {
    --color-text: black;
    --color-background: white;
    --color-primary: rebeccapurple;
  }
`;

const App = ({ children }) => {
  return (
    <>
      <GlobalStyles />
      {children}
    </>
  );
};

(만약 당신이 createGlobalStyle에 익숙하지 않다면, 우리가 styles.css 파일에 쓰는 것처럼 범위가 지정되지 않은 CSS를 쓸 수 있습니다.)

몇 가지 변수를 생성하여 루트 노드에 연결했고, 이제 컴포넌트에서 액세스할 수 있습니다.

// JSX
const Button = styled.button`
  background: var(--color-primary);
`;

제 생각에 이것은 작긴해도 성공적인 결과입니다. import 또는 인라인 함수 없이 테마 값에 액세스할 수 있다는 것은 마치 신선한 공기를 불어넣는 것 같습니다. 정적 타이핑의 이점(나중에 자세히 설명)을 잃게 되지만 제 생각에는 매우 만족스러운 절충안입니다.

그러나 이것은 비교적 사소한 차이입니다. 좀 더 흥미로운 것을 살펴볼까요?

변수가 아니라 값을 변경합니다

Button 컴포넌트가 있다고 가정해 보겠습니다.

괜찮아 보이지만, 모바일 기기에서 클릭 범위가 너무 작다는 피드백을 받았습니다. 업계 가이드라인에 따르면 양방향 엘리먼트의 높이는 44~48픽셀 사이여야 합니다. 모바일 기기에서 탭하기 쉽도록 크기를 키워야 합니다.

CSS 변수를 사용하지 않고 가능한 해결 방법을 살펴보겠습니다.

// JSX

const Button = styled.button`
  /* 간결하게 보이기 위해 다른 스타일은 생략 */
  min-height: 32px;

  @media (pointer: coarse) {
    min-height: 48px;
  }
`;

이 코드에 대한 간단한 두가지 참고사항입니다.

  • 필요한 경우 버튼이 커질 수 있도록 height 대신 min-height를 사용하고 있습니다. 이는 사용자가 기본 글꼴 크기를 높이거나 텍스트를 줄 바꿈 해야 하는 경우에 발생할 수 있습니다.
  • 너비 기반(width-based) 미디어 쿼리 대신 pointer: coarse를 사용하고 있습니다. 이 미디어 쿼리는 사용자의 기본 입력 메커니즘이 정밀한지 거친지를 추적합니다. 마우스나 트랙패드는 픽셀에 대한 위치를 제어할 수 있기 때문에 "괜찮은" 것으로 간주되지만, 손가락이나 Wii 리모컨은 정확도가 떨어집니다. 우리는 실제 화면 크기에는 관심이 없습니다. 정확히 클릭하거나 탭할 수 있는지 여부에 관심이 있습니다.

우리는 이 변경 사항을 반영하고, 앱의 사용성이 향상되었다는 것을 알고 나면 전보다 편안하게 잠들 수 있습니다.

하지만 우리의 일이 끝나지 않았다는 것을 금방 알게 됩니다. 앱에서 탭이 가능한 엘리먼트는 버튼 뿐만이 아닙니다! 다른 것보다도 텍스트 입력이 있습니다.

TextInput 컴포넌트도 업데이트 하겠습니다. DRY 상태를 유지하기 위해 테마에 크기를 저장합니다.


// JSX

const App = ({ children }) => {
  return (
    <ThemeProvider
      theme={{
        colors: COLORS,
        coarseTapHeight: 48,
        fineTapHeight: 32,
      }}
    >
      {children}
    </ThemeProvider>
  );
};

두 컴포넌트 모두 다음 값을 사용합니다.

```jsx

const Button = styled.button`
  min-height: ${(props) => props.theme.fineTapHeight}px;

  @media (pointer: coarse) {
    min-height: ${(props) => props.theme.coarseTapHeight}px;
  }
`;

const TextInput = styled.input`
  min-height: ${(props) => props.theme.fineTapHeight}px;
  
  @media (pointer: coarse) {
    min-height: ${(props) => props.theme.coarseTapHeight}px;
  }
`;

이것은 탭할 수 있는 모든 엘리먼트에 연결하기 위한 중요한 CSS 청크입니다!

이제 styled-component mixin 또는 CSS 클래스를 사용하여 여러 가지 방법으로 이 문제를 해결할 수 있습니다. 하지만 이 문제를 해결하는 가장 좋은 방법은 CSS 변수를 사용하는 것입니다.

각 엘리먼트가 서로 다른 breakpoint에서 어떻게 대응해야 하는지를 강제로 지정하는 대신, 이를 추적하는 reactive 변수를 전달하면 어떨까요?

// JSX
const GlobalStyles = createGlobalStyle`
  html {
    --min-tap-target-height: 32px;

    @media (pointer: coarse) {
      --min-tap-target-height: 48px;
    }
  }
`;

이러한 마법같은 CSS 변수를 사용하면, 반응형 컴포넌트가 훨씬 더 간단해집니다.

// JSX
const Button = styled.button`
  min-height: var(--min-tap-target-height);
`;

const TextInput = styled.input`
  min-height: var(--min-tap-target-height);
`;

처음으로 인정하겠습니다. 이 패턴을 처음 보면 좀 이상해보입니다. 멘탈 모델 전환이 필요한 상황이네요. 하지만 제 생각에는 매우 매력적입니다!

우리 모두는 CSS 미디어 쿼리를 사용하여 다양한 시나리오에서 다양한 CSS 청크를 적용하는 데 익숙합니다. 그러나 CSS 속성을 동일하게 유지하고 값을 변경하면 어떨까요?

컴포넌트 내에서 min-height는 항상 동일한 값인 --min-tap-target-height를 가리키지만 해당 값은 동적입니다.

몇 가지 이점을 살펴보겠습니다.

  • breakpoint 항목을 한 곳으로 통합함으로써 단일화된 소스 코드를 가질 수 있습니다. 이전에는 제멋대로인 개발자가 breakpoint 중 하나를 실수로 삭제하여 일관성 없는 동작이 발생할 수 있었지만, 이제 탄력적인 변수로 패키징되었습니다.
  • 우리가 이 일을 하는 이유를 좀 더 명확히 할 수 있습니다. 처음에 min-height라는 이름을 붙였는데, 이는 애초에 min-tap-target-height 값을 설정해야하는 이유를 보여줍니다.
  • 더 선언적입니다! 포인터 유형에 따라 각 엘리먼츠가 어떻게 변경되어야 하는지 지정하는 대신, 값이 무엇인지 알려줍니다.

"Principle of Least Knowledge(최소 지식의 원칙)""은 코드가 코드베이스의 완전히 다른 부분에 "도달"하지 않고 바로 인접한 항목에만 액세스할 수 있어야 한다는 생각입니다. 조금 흐린 눈을 하고 보면, 같은 아이디어가 여기에도 적용되는 것 같습니다.

또 다른 간단한 예는 테마 변수에 대해 동일한 트릭을 수행할 수 있으므로, 각 viewport가 고유한 척도를 갖도록 하는 것입니다.

// JSX
const GlobalStyles = createGlobalStyle`
  html {
    --space-sm: 8px;
    --space-md: 16px;
    @media (min-width: 1024px) {
      --space-sm: 16px;
      --space-md: 32px;
    }
  }
`;

// 다른 곳에서는...
const Paragraph = styled.p`
  padding: var(--space-sm);
`;

모든 window 크기에 대해 동일한 배율을 사용하는 대신, 각 breakpoint에 대해 사용자 지정 배율을 사용할 수 있습니다. 이를 통해 사용자 인터페이스가 훨씬 일관적으로 유지되고 각 컴포넌트 내부에서 문제를 전보다 쉽게 해결할 수 있습니다.

다른 새로운 가능성이 있습니다

지금까지 우리가 이야기한 모든 것은 개발자 경험에 관한 것입니다. 우리는 문제를 해결하기 위한 대안을 살펴보았습니다.

이제 CSS 변수가 고유해짐에 따라, 사용자 경험 개선으로 이어지는 몇 가지 문제를 살펴보겠습니다.

모든 속성에 애니메이션 적용하기

간단히 애니메이션화할 수 없는 일부 CSS 프로퍼티가 있습니다. 예를 들어 선형 또는 방사형 그래디언트에 애니메이션을 적용하려고 시도한 적이 있다면, 애니메이션이 작동하지 않는다는 것을 금방 알아차리셨을 겁니다.

하지만 CSS 변수를 사용하면, 프로퍼티에 전환을 적용하는 것이 아니라 값에 전환을 적용하므로 모든 프로퍼티에 애니메이션을 적용할 수 있습니다.

예를 들어, 다음은 CSS 변수 덕분에 애니메이션이 가능해진, 재미있는 그라데이션 애니메이션입니다.

이 글을 쓰는 지금, 이 애니메이션은 Firefox 또는 Safari에서 작동하지 않습니다. CSS 변수는 광벙위하게 지원되지만 브라우저는 여전히 변환을 적용하는 기능을 구현하고 있습니다. 이것은 CSS 엔진을 개발자에게 드러내기 위한 수년간의 엄청난 노력인 CSS Houdini의 일부입니다.

이 버튼에 대한 자세한 내용은 제 튜토리얼인 "Magic Rainbow Gradients"를 참조해주세요.

"다크 모드" 플래시 수정

만약 여러분이 "다크 모드" 변형을 구현하려고 시도했다면, 아마도 이 까다로운 상황에 닥쳤을 것입니다. 짧은 순간, 잘못된 색상이 깜박입니다:


< 페이지가 로드될 때의 '조명 모드 플래시' >

"다크 모드"는 특히 Next.js 또는 Gatsby와 같은 서버 렌더링 컨텍스트에서 놀랍도록 까다롭습니다. 여기서 문제는, HTML이 사용자 기기에 도달하기 훨씬 전에 생성되기 때문에 사용자가 어떤 색상의 테마를 선호하는지 알 방법이 없다는 것입니다.

우리는 CSS 변수와 약간의 묘수으로 이 문제를 해결할 수 있습니다. 제 블로그 게시물 "The Quest for the Perfect Dark Mode"에 이 접근법에 대해 썼습니다.

Getting과 Setting

위의 예시에서는 테마 값을 GlobalStyle 컴포넌트에 하드 코딩합니다.

// JS
const GlobalStyles = createGlobalStyle`
  html {
    --color-text: black;
    --color-background: white;
    --color-primary: rebeccapurple;
  }
`;

자바스크립트에서 이러한 값에 접근해야 하는 경우가 있을 수 있습니다.

원하는 경우 constant.js 파일에 계속 저장할 수 있습니다. 그 파일은 CSS 변수를 인스턴스화하는 데 사용되지만 JS에서 raw value가 필요한 곳이면 어디든지 가져올 수 있습니다.

// JS
const GlobalStyles = createGlobalStyle`
  html {
    --color-text: ${COLORS.text};
    --color-background:  ${COLORS.background};
    --color-primary:  ${COLORS.primary};
  }
`;

그러나 또 다른 아이디어는 CSS를 진실의 출처(source of truth)로 사용하는 것입니다. 약간의 JS를 사용하여 값에 액세스할 수 있습니다.

// JS
getComputedStyle(document.documentElement)
  .getPropertyValue("--color-primary");

JS에서 이러한 값을 설정할 수도 있습니다.

// JSX
document.documentElement.style.setProperty(
  "--color-primary",
  "hsl(245deg, 100%, 60%)"
);

JS에서 CSS 변수를 가져오고 설정하는 것은 탈출구입니다. 얼마나 드물게 사용되는지 알면 놀랄 겁니다! 심지어 임베디드된 SVG 내에서 CSS 변수를 사용할 수도 있습니다 😮

단점

타입이 없습니다

아마도 테마에 CSS 변수를 사용할 때 가장 큰 단점은 (Typescript 또는 Flow를 통해) 테마를 정적으로 입력할 방법이 없다는 것입니다.

제 생각에 이것은 큰 문제는 아닙니다. 저는 양쪽을 다 경험해보았습니다. 타입이 지정된 테마 오브젝트가 있는 것은 좋지만 시간이 많이 절약되었다고 말할 수는 없습니다. 일반적으로 CSS 변수의 이름을 잘못 입력하는 것은 쉽게 알 수 있으며 빠르게 수정이 가능합니다.

사이트에서 compile-time check를 실행하는 것이 중요하다고 생각하지만 Chromatic과 같은 도구가 훨씬 더 신뢰할 수 있는 검사라고 생각합니다. CI에서 실행되며 렌더링된 시각적 출력의 차이점을 캡처하기 때문입니다.

그렇긴 하지만, type-safety가 필수 항목이라면 포기할 필요가 없습니다! 스타일을 JS 객체에 유지하고 덧붙이기만 하면 됩니다. Fatih Kalifa의 이 트윗은 그가 CSS 변수에 대한 타입을 설정하는 방법을 보여줍니다.

IE 브라우저는 지원하지 않습니다

CSS 변수는 4개의 주요 브라우저에서 정상적인 브라우저 지원을 받고 있지만 IE 지원은 없습니다.

느슨하지 않습니다

styled-components를 사용할 때, 미디어 쿼리를 포함하여 원하는 곳에 변수를 넣을 수 있습니다.

// JSX
const Ymca = styled.abbr`
  font-size: 1rem;
  @media (max-width: ${(p) => p.bp.desktop}) {
    font-size: 1.25rem;
  }
`;

CSS 변수는 미디어 쿼리 내에서 사용할 수 없습니다. 사용자가 env()를 사용하여 자신의 환경 변수를 설명할 수 있도록 하는 것에 대해 많은 이야기가 있습니다만.. 아직 실현되지는 않았습니다.

그러나 이러한 단점에도 불구하고 CSS 변수는 정말 흥미로운 점이 많습니다. 저는 그것들을 몇 년 동안 사용해 왔고 (바로 이 블로그를 포함하여!) 그것들을 사랑합니다. 다이나믹한 스타일을 관리하는 데 있어 제가 선호하는 방법입니다.

성스러운 삼위일체

웹 개발자로서 우리는 HTML, CSS, JS의 3가지 주요 기술을 사용합니다. 효과적으로 일을 하기 위해서는 세 가지 모두에 대해 편안함을 느껴야합니다.

저는 HTML과 JS에 대해 꽤 확실하게 이해하고 있지만 CSS에 어려움을 겪는 개발자들을 많이 알고 있습니다. 저 역시도 몇 년간 그 사람들 중 하나였습니다!

CSS는 믿을 수 없을 정도로 복잡한 언어입니다. 기본은 빠르고 (상대적으로) 쉽게 배울 수 있지만 마스터하기에는 엄청나게 어려운 언어입니다.

우리가 CSS를 작성할 때, 우리는 빙산의 일각만 볼 수 있습니다. CSS는 매우 함축적인 언어입니다. 모든 종류의 비밀 메커니즘은 놀랍고 이해할 수 없는 방식으로 CSS 속성의 효과를 조정하면서 작동합니다.

CSS에는 오류 메시지나 콘솔 로그가 없습니다. 우리는 CSS를 작성하는데, 결과가 기대하는대로 나오기도 하고 아니기도 합니다. 일이 잘못된다면 우리를 무력감에 빠질 수 있습니다. 우리는 그들 중 한 명이 우리가 원하는 것을 해주기를 기대하며, 무작위로 CSS 속성을 벽에 던지게 됩니다.

저는 도와드리고 싶습니다. 저는 최근에 JavaScript 개발자를 위한 CSS 과정인 CSS 코스를 발표했습니다. React와 같은 JS 프레임워크를 사용하는 사람들을 위해 특별히 제작되었습니다. 언어에 대한 직관을 개발할 수 있도록 멘탈 모델을 구축하기 위해 CSS가 실제로 어떻게 작동하는지 알려줍니다.

당신이 들었던 다른 코스들과는 다릅니다. 150개가 넘는 비디오가 있지만, 그것은 비디오 코스가 아닙니다. 이것은 상호작용할 수 있고, 다중 모드이며, 스스로 떠나는 모험과 같습니다. 수 많은 연습, 실제 세계에서 영감을 받은 프로젝트, 그리고 몇 개의 미니 게임도 있습니다. 이전에 사용했던 것과는 다른 맞춤형 플랫폼을 사용합니다.

CSS 때문에 어려움을 겪으신다면 꼭 확인해보시길 바랍니다. CSS로 자신감을 얻는 것은, 특히 HTML과 JS에 이미 익숙하다면 판도를 바꾸는 일입니다. 성스러운 삼위일체를 완성하면, 이 흐름 속에 머무르는 것이 훨씬 쉬워지고, 웹 애플리케이션을 개발하는 것을 진정으로 즐길 수 있게 됩니다.

자세히 보시려면
https://css-for-js.dev

profile
Software Developer

0개의 댓글