변화에 강한 styled-component 코드 작성하기

pengooseDev·2023년 3월 14일
2
post-thumbnail

네? 값이 바뀔 수 있다구요?

디자이너님과 함께 메인 페이지 작업을 시작했다.
결론부터 말하자면 디자인은 총 5번이 바뀌었다. 어흑...

첫 디자인 변경당시, 하드코딩 된 값들을 제거하는 작업을 하며, 디자이너님과 소통하는 과정에서 아래의 사실을 깨닫게 되었다.

  1. CSS에 대한 값은 정해져있다. 요구사항(figma)과 일치하는 CSS 값으로 코드를 작성할 것.
  2. 다만, 그 요구사항은 상황에 따라 변할 수 있다.

요컨대, 정확한 수치가 주어져있지만, 그 수치가 언제든 바뀔 수 있다는 뜻이다.
모순과 같은 이 상황을 어떻게 해결할 것인가?


소프트 코딩

가장 먼저 생각난 해결책은 소프트 코딩이다.
자주 변할 수 있는 부분(넓이, 높이, 색상 등)을 Constant 파일에 선언해두고 import해 전역적으로 관리하는 방법이다.

constant/navigation.ts

/* 아래와 같이 NAV 컴포넌트의 속성을 선언해둔다면, 해당 값이 바뀔 때, 
   영향을 받는 컴포넌트들을 소프트코딩한다면 변화에 쉽게 대처가 가능하다! :) */
export const NAV = Object.freeze({
  WIDTH: 60,
  BORDER_WIDTH: 1,
  BACKGROUND: '#D9D9D9',
  GAP: 10,
});

shared/contentWrapper.tsx

import styled from 'styled-components';
import { SIDE_NAV, TOP_NAV } from 'constants/';
import { DEVICES } from 'styles';
import { useLocation } from 'react-router-dom';
import { useMediaQuery } from 'hooks';
import { navChecker } from 'libs';
import { useSelector } from 'react-redux';
import { RootState } from 'redux/store';

export function ContentWrapper({ children }: { children: React.ReactNode }) {
  const isNotSmallDevice = useMediaQuery(DEVICES.MOBILES);
  const { pathname } = useLocation();
  const hasNav = navChecker(pathname);
  const isLoading = useSelector((state: RootState) => state.authLoadingSlicer);

  return (
    <Wrapper
      isNotSmall={isNotSmallDevice}
      hasNav={hasNav}
      isLoading={isLoading}
    >
      <GridBox>{children}</GridBox>
    </Wrapper>
  );
}

interface WrapperProps {
  isNotSmall: boolean;
  hasNav: boolean;
  isLoading: boolean;
}

const Wrapper = styled.div<WrapperProps>`
  position: fixed;
  top: ${(props) => (props.hasNav ? TOP_NAV.HEIGHT : 0)}px;
  left: ${(props) =>
    props.isNotSmall && props.hasNav && !props.isLoading
      ? `${SIDE_NAV.WIDTH}px`
      : '0px'};
  width: ${(props) =>
    props.isNotSmall && props.hasNav && !props.isLoading
      ? `calc(100% - ${SIDE_NAV.WIDTH}px - ${TOP_NAV.PADDING * 2}px)`
      : `calc(100% - ${TOP_NAV.PADDING * 2}px)`};
  height: ${(props) =>
    props.hasNav
      ? `calc(100% - ${TOP_NAV.HEIGHT}px - ${TOP_NAV.PADDING * 2}px)`
      : `calc(100% - ${TOP_NAV.PADDING * 2}px)`};
  padding: ${TOP_NAV.PADDING}px;
`;

const GridBox = styled.div`
  display: grid;
  height: 100%;
  border-radius: 10px;
`;

단순히 px의 변경뿐 아니라 해당 페이지에 Navbar가 있는지, 로딩중인지, 디바이스 크기가 적당한지 등을 판별하고 모든 경우의 수에 맞는 유동적인 CSS를 제공할 수 있다.

코드를 작성하고 "이 정도면 괜찮게 해결했을지도..?"라는 생각이 들 때, 한 가지 문제점을 발견했다.
나의 코드는 Theme Toggle이 들어갈 때 굉장히 취약하다는 것. 😨 헉...

결국 GlobalStyle에 모든 값들을 선언해야하나?라는 생각이 들었다.

하지만, 해당 방법은 컴포넌트가 늘어날수록 themeToggle의 코드 길이가 굉장히 길어지기 때문에 명확한 해결책이라 볼 수 없다.


결국은 객체. 관심사의 분리

GlobalStyle과 constant의 조합

곰곰히 생각해보았다.
디자이너님이 정해주는 값은 크게 두 가지로 나뉜다.

1. 자주 바뀌는 값

2. 자주 안바뀌는 값.

1번. 즉, 자주 바뀌는 값은 소프트 코딩의 대상이다.
1번은 또 다시 아래처럼 나뉜다.

1-1. 자주 바뀌면서 theme과 관련이 있는 값

- color, backgroundColor 등

1-2. 자주 바뀌면서 theme과 관련이 없는 값

- width, height, border-radius 등

해결책

Theme state에 영향을 받는 값(1-1)

styled-components의 GlobalStyle에서 변수로 선언해 전역적으로 관리하기로 결정했다.
상위 ThemeProvider에서 현재 theme의 state와 색상에 따라 값을 변경해주어야 하기 때문이다.

Theme state에 영향을 받지 않는 값(1-2)

이전과 동일하게 constant로 관리하기로 했다.

theme state에 의해 변하지 않는다는 점에서 어느 곳에 관리해도 상관없지만, GlobalStyle의 theme에서 관리할 경우 객체의 관심사가 일치하지 않는다고 판단했고, lightTheme과 darkTheme에 중복하여 선언을 해야하기 때문에 힙 메모리가 낭비된다고 생각했기 때문이다.

결과는 아래와 같다.


shared/Nav.tsx

import styled from 'styled-components';
import { NAV } from 'constants/layout';

function Nav() {

  return (
    <Wrapper>
      // 컴포넌트
    </Wrapper>
  );
}

export default Nav;

const Wrapper = styled.div`
  position: fixed;
  left: 0;
  top: 0;
  background: ${({ theme }) => theme.navBackground};
  box-sizing: border-box;
  border-right: black ${NAV.BORDER_WIDTH}px solid;
  width: ${NAV.WIDTH}px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 23.5px;
  gap: ${NAV.GAP}px;
`;

constant/navigation.ts

export const NAV = Object.freeze({
  WIDTH: 88 as const,
  BORDER_WIDTH: 1 as const,
  GAP: 24,
});
//background가 빠짐!

styles/theme.ts


export const darkTheme: DefaultTheme = {
  background: '#2b2b2b',
  transparentBackground: 'rgba(43, 43, 43, 0.65)',
  color: '#F5F6F7',
  transparentColor: 'rgba(245, 246, 247, 0.65)',
  pointColor: '#FF681B',
  transitionOption: 'ease-in-out 0.15s',

  /* nav */
  navBackground: '#D9D9D9',
  navLinkBackground: '#666666',
};

export const lightTheme: DefaultTheme = {
  background: '#F5F6F7',
  transparentBackground: 'rgba(245, 246, 247, 0.65)',
  color: '#2b2b2b',
  transparentColor: 'rgba(43, 43, 43, 0.65)',
  pointColor: '#FF681B',
  transitionOption: 'ease-in-out 0.15s',

  /* nav */
  navBackground: '#666666',
  navLinkBackground: '#D9D9D9',
};

삼항연산자로 각 컴포넌트에서 해결할까 생각도 해보았지만, 규모가 커질 경우 재사용성이 제한된다고 판단해, 해당 방법을 채택하였다 :)

0개의 댓글