Prologue
공교롭게도 지난번 글이 '서비스에 Styled-Components를 적용했던 썰' 이었는데, 이번 글이 'Styled-Components를 걷어내고 emotion을 도입하게 된 썰'이 될 줄은 몰랐지만 😂 아무튼! 이번 경험담 또한 스타일에 관한 이야기다.
잠깐 배경 설명을 하자면, 우리 팀은 입사하기 전까지 SCSS로 스타일을 작성하고 있었다. 그리고 팀 내에서는 CSS-in-JS 도입에 관한 논의를 진행하고 있었는데, 마침 내가 입사하면서 도입을 시도하게 됐으니 그것이 바로 Styled-Components였다. 공통 스타일 변수부터 전역 스타일까지 모든 세팅을 마치고 푸터와 레이아웃 일부에 이를 도입하며 성공적으로 안착하는 듯 했으나, 방대한 기존 스타일을 대체하기엔 다소 맞지 않다는 판단 하에 재논의를 거쳐 최종으로 결정된 것이 바로 이 emotion이라는 라이브러리이다.
Why emotion?
emotion과 Styled-Components에 대한 비교 글들을 보면 알겠지만 큰 틀에서는 같은 CSS-in-JS라는 점에서 흡사하다. 심지어 emotion에는 emotion/styled 라는 유사 Styled-Components(?)가 있기도 한데, 그럼에도 emotion으로 변경하게 된 계기를 설명하고자 한다.
1. css props
단어 그대로 css를 props 형식으로 사용할 수 있는 방식이다. 클래스네임을 작성해 해당 클래스네임에 스타일을 부여하는 방식과 유사하다고 볼 수 있다. 물론 공식 문서에는 인라인으로 작성하는 방식을 먼저 소개하고 있긴 하지만 일반적으로는 다음과 같은 방식으로 작성한다.
import { css } from '@emotion/react';
const base = css`
color: hotpink;
`
export const box = () => {
return (
<div css={`${base}`}>
This is hotpink.
</div>
)
}
위 코드 예시를 보면 알 수 있듯 css props는 마치 클래스네임과 같은 역할을 하고 `${base}` 라는 변수에 스타일을 작성해 적용하는 것이다. 이 방식을 통해 공통 스타일이나 중복 스타일들을 css props로 다양하게 전달해 스타일을 재사용하거나 조합하기 용이하게 된다.
2. SSR
기존 Styled-Components는 SSR을 위해서는 ServerStyleSheet을 설정해야 하는 번거로움이 있었다. 하지만 emotion에서는 별도 설정을 하지 않아도 SSR에서 사용할 수 있다는 장점이 있다.
그 외에도 emotion이 Styled-Components에 비해 조금 더 작고 가볍다는 내용도 있긴 하지만 유의미한 차이가 있진 않기에 고려할 점은 아니었다. 아무튼 이러한 차이점을 토대로 조금 더 편리하고 기존 코드 스타일을 유지하며 작성할 수 있다는 장점 하에 변경하기로 결정했다.
지금부터 시작이다
emotion을 도입하기로 결정한 후 emotion 내 라이브러리 중 우리가 처음 선택한 라이브러리는 @emotion/react 였다. 리액트 전용 emotion 라이브러리인만큼 여러 편의성을 제공하고 위에서 언급한 SSR 적용이 가능하다는 점에서 도입하기로 했다.
하지만 도입 후 팀원들과 논의를 하며 몇 가지 피드백을 받았는데, 이는 다음과 같다.
1. 공통 네이밍을 만들기 어렵다.
개인 프로젝트를 진행해 컴포넌트 개수나 규모가 작다면, 혹은 팀원이 적다면 문제가 없을 것이다. 하지만 팀원도 많고 컴포넌트 규모도 크다면 이는 분명 문제가 될 수 있다. 자식 요소들을 감싸는 부모 컨테이너를 지칭함에 있어서도 wrapper, container, box 등등 여러 네이밍이 존재하는데 이러한 규칙이 모호해 통일성을 저해시킬 수 있다는 의견이 있었다.
2. nesting이 쉽지 않다.
그동안 SCSS로 작성할 때는 여러 자식 요소들을 nesting하며 스타일을 부여했는데 emotion에서는 이 점이 SCSS만큼 자유롭지 못했다. 물론 각 요소들이 중첩되지 않고 스타일을 할 수 있다면 그 방법이 제일 좋겠지만, 이미 이러한 방식으로 작성된 수많은 스타일 코드들을 분리하기에는 시간이 부족하기도 하고 쉽지 않을 것 같았다.
이러한 의견들을 반영해 우리 서비스에서는 기존 emotion에서 사용하는 방식과는 조금 다른 방법을 적용하기로 했다. 바로 @emotion/css 와 BEM 방법론, CSS 모듈 방식을 사용하는 것이었다.
@emotion/css ?
@emotion/react가 리액트를 기반으로 하는 프로젝트에 여러 편의 기능을 함께 제공하는 패키지였다면, @emotion/css는 라이브러리, 프레임워크에 관계 없이 사용 가능한 패키지이다. 몇 가지 편의 기능은 사용할 수 없지만 환경설정이 크게 필요 없다는 장점이 있고, 무엇보다 'className'을 사용할 수 있다는 점이 우리 팀에게 매력적으로 느껴졌다. 그 차이점을 잠시 보자면 다음과 같다.
// @emotion/react
import { css } from '@emotion/react'
const color = 'darkgreen'
render(
<div
css={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
This has a hotpink background.
</div>
)
// @emotion/css
import { css } from '@emotion/css'
const color = 'darkgreen'
render(
<div
className={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
This has a hotpink background.
</div>
)
@emotion/react는 css props 방식을 통해 요소에 스타일을 전달한다면, @emotion/css는 기존 css와 동일하게 className으로 해당 스타일을 주입할 수 있었다. 이를 통해 두번째 방식인 BEM 방법론을 접목시킬 수 있었다. BEM을 emotion에 접목시킨 예시는 다음과 같다.
// Box.style.ts
import { css } from '@emotion/css';
export const cssBox = css`
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
&__text {
font-size: 20px;
color: tomato;
}
`
// Box.tsx
import { cssBox } from './Box.style';
const Box = () => {
return (
<div className={cssBox}>
<span className={`${cssBox}__text`}>텍스트</span>
</div>
)
}
위 코드를 보자면 Box.style.ts라는 css module을 만들어 해당 파일에 다음과 같이 BEM 방식으로 스타일을 작성하고 이를 Box.tsx에서 불러와 클래스네임에 주입하는 방식이라고 할 수 있다. 여기서 중요하게 봐야 할 점은 BEM을 위해 템플릿 리터럴로 클래스네임을 부여했다는 점이다. 위와 같이 템플릿 리터럴로 작성하면 cssBox가 해쉬화되며 달라져도 뒤에 붙는 element, modifier 부분은 그대로 유지될 수 있다.
자 이렇게 emotion을 적용할 기반을 마련했다면, 이제 공통 스타일을 만들어줄 차례다.
먼저 색상, 폰트 사이즈, z-index 등 팀 내에서 공통으로 사용하는 스타일 변수는 객체 형식으로 제작해 import해 사용하는 방법을 적용했다.
// variables.style.ts
export const colors = {
red: '#ed2040'
}
// Box.style.ts
import { css } from '@emotion/css';
import { colors } from '@/styles/variables.style';
export const cssBox = css`
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
&__text {
font-size: 20px;
color: ${colors.red};
}
`
마지막 &__text 쪽을 보면 css 템플릿 리터럴 내에 다음과 같이 해당 변수 객체 키를 넣어 스타일을 주입하는 것을 알 수 있다. 이러한 방식으로 스타일 변수 뿐 아니라 공통 keyframes도 분리해 관리할 수 있었다.
마지막으로 공통으로 쓰는 스타일 중 가장 많이 활용하는건 아무래도 미디어쿼리일 것이다. 팀 내에서는 모바일, 태블릿, 데스크톱 등 각각 breakpoint를 지정해 미디어쿼리를 사용하고 있었는데, SCSS에서는 @mixin이라는 편의 기능을 제공하지만 emotion에서는 직접 함수로 제작해 사용해야 했다. 이에 다음과 같은 방식으로 함수를 제작해 사용했다.
const mediaQuery = (breakPoint: keyof typeof size, direction?: 'reverse') => {
const viewport = {
mobile: `max-width: ${size[breakPoint] - 1}px`,
desktop: `min-width: ${size[breakPoint]}px`,
};
return `@media (${direction ? viewport.mobile : viewport.desktop} )`;
}
breakPoint 파라미터에는 스타일 변수로 지정한 값이 들어오고, 두번째 파라미터인 direction에 'reverse'를 입력하면 max-width로, 없다면 min-width로 미디어 쿼리를 적용하는 방식이다. 우리 팀은 mobile first를 지향하기 때문에 다음과 같이 max-width는 옵셔널하게 사용하도록 작성한 것이다. 이를 스타일 코드에 적용하면 다음과 같은 모습이 된다.
// Box.style.ts
import { css } from '@emotion/css';
import { mediaQuery } from '@/styles/mixin.style';
import { colors } from '@/styles/variables.style';
export const cssBox = css`
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
&__text {
font-size: 20px;
color: ${colors.red};
}
${mediaQuery('medium')} {
width: 200px;
height: 200px;
&__text {
font-size: 30px;
}
}
`
지금까지 emotion을 도입하기 위해 했던 과정에 대해 알아봤다. 쉽지 않은 과정들이었지만 그 과정을 통해 이렇게 새로운 기술과 방식을 도입해 팀에 기여할 수 있어서 참 뿌듯한 순간이었다. 물론 stylelint 적용 문제, mixin 마이그레이션 등 앞으로 해나갈 문제들은 많지만 이 또한 이번 과정을 통해 체득한 삽질(?) 노하우로 헤쳐나갈 수 있을거라 생각한다.
'Programming > 3. Experience' 카테고리의 다른 글
useReducer + custom hook으로 state 관리하기 (0) | 2024.07.26 |
---|---|
emotion to scss module 도입기 (0) | 2024.07.25 |
SCSS에서 Styled-Components로 변환하며 겪은 썰.txt (0) | 2022.05.16 |
[연습] Momentum 응용하기 (0) | 2021.06.23 |
[연습] HTML, CSS로 계산기 만들기 (1) | 2021.05.02 |
댓글