Styled Components를 사용하며..

컴포넌트 구성하기

Styled Component를 사용할 때 어떻게 컴포넌트 코드를 구성해야 하는지 고민이 되었다.

코딩 컨벤션은 주관적인 요소가 많아서 확실한 정답이 존재하지 않는다. 다만, 중요한 것은 일관성과 협업을 위한 코드의 가독성이다. 코드의 가독성을 위해 Styled Component에서 사용하는 코딩 컨벤션을 알아보았다.

1. 컴포넌트와 같은 파일에 스타일 정의하기

스타일을 정의하고 사용하는 컴포넌트와 같은 파일에 직접 작성하는 방식이다. 스타일과 컴포넌트가 밀접하게 연결되어 있을 때 유용하며, 작은 규모의 프로젝트나 컴포넌트에서 자주 사용한다.

import styled from 'styled-components'

export default function Component() {
...
  return (
    <div>
      ...
      <StButton>...</StButton>
    </div>
  )
}
            
// Style 관련 코드는 하단에
const StButton = styled.button`
  padding: 0.5rem 0;
  ...
`

2. 컴포넌트와 스타일 분리 (폴더 구조화)

Styled Component를 작성할 때, 스타일과 로직을 분리하는 방법이다. 스타일은 따로 정의하고 로직을 구성하는 컴포넌트에서는 스타일을 import해서 사용하는 방식으로 구성한다. 프로젝트가 커지면 컴포넌트와 스타일 파일을 구조화하는 방법도 있다.

// Example 1
components/
	Button/
		Button.jsx
		Button.styled.js
// Example 2
components/
	Button/
		index.js
		styles.js

▶ import 방식

import * as S from "./Button.styled.js"
import * as Styled from "./styles.js"

...
return <S.ButtonContainer >
	<S.ButtonContent>
	</S.ButtonContent>
</S.ButtonContainer>

3. 각 장단점 비교하여 컴포넌트 구성 방법 결정하기

3.1 컴포넌트와 같은 파일에 스타일 정의하기

  • 장점
    • 컴포넌트 자체를 다른곳에서 재사용하기 용이
    • 별도의 스타일 파일을 만들지 않으므로 프로젝트의 파일 구조가 단순
  • 단점
    • 컴포넌트 코드가 길어질 수 있으며, 스타일과 로직이 같이 존재해 가독성이 떨어짐

3.2 컴포넌트와 스타일 분리 (폴더 구조화)

  • 장점
    • 스타일을 별도의 파일로 관리함으로써, 여러 컴포넌트에서 스타일 재사용 용이
    • 코드의 가독성과 유지보수성이 향상
  • 단점
    • 파일 구조가 복잡해지고, 파일 수가 많아져 프로젝트 관리의 어려움

이를 토대로 이번 프로젝트에서는 컴포넌트와 같은 파일에 스타일을 정의하는 방식으로 스타일링 하였다. 작은 프로젝트이기도 하고 프로젝트 구조가 복잡해지면 파일을 찾고 여러 파일을 오가며 작업해야 하므로 작업 효율이 떨어진다고 생각하였다. 스타일 컴포넌트의 스타일은 컴포넌트 하단에 정의하여 가독성 문제를 해결하고자 했다.

Styled Components를 사용하여 개발을 하던 중 문제가 발생했다. 사이드바의 카테고리가 현재 URL과 일치하면 스타일을 다르게 적용하기 위해 NavLink를 사용하고자 했다. 기존까지는 CSS 변수에 색상이나 넓이, 길이 등의 값을 넣어서 사용했는데 NavLink의 style에서는 :root에 정의된 CSS 변수에 접근할 수 없었다.

<NavLink
  to={`/topics/${category.name}`}
  style={({ isActive }) => ({
    backgroundColor: isActive
      ? 'rgba(207,210,226,0.2)'
      : 'inherit',
    color: isActive ? var(--category-text) : 'inherit' // ❌ 불가능
  })}
>
  <p>{category.name}</p>
</NavLink>

다른 방법을 찾다가 공식 문서를 살펴보니 NavLink가 활성화되면 active 클래스가 추가된다는 것을 알 수 있었다.

NaLink v6.22.3 | React Router

image-20240403173922442

NavLinkstyled-components의 컴포넌트 확장을 이용해 스타일링한다. CSS 클래스 선택자를 사용해 .active 클래스에 스타일을 적용하였다. 스타일 컴포넌트에서 활성화된 스타일을 작성하여 스타일이 분리되니 가독성이 좋아졌다.

<StMajorNavLink to={`/topics/${category.name}`}>
 <p>{category.name}</p>
</StMajorNavLink>

const StMajorNavLink = styled(NavLink)`
  margin-bottom: 3px;
  padding: 7px 12px 7px 20px;
  display: block;
  width: 100%;
  border-radius: 100px;
  font-weight: bold;

  &.active {
    background-color: var(--category-bg);
    color: var(--text-color);
  }
`;

CSS 변수 ➡ Theme를 이용한 스타일 변수 관리

CSS 변수를 사용했을 때의 불편함

CSS 변수는 CSS에서 직접 정의하고 사용할 수 있는 변수이다. CSS를 사용하면 같은 값을 여러 번 사용해야 할 때 편리하고 변수의 값만 변경하면 해당 변수를 사용하는 모든 곳에 적용된다. 이는 전체적인 스타일의 일관성을 유지하면서 유지보수의 효율성을 크게 향상시킨다.

CSS 변수를 사용하면 CSS와 JavaScript 사이의 경계를 확실히 하고 브라우저가 직접 변수를 관리하여 성능의 이점이 있을 수 있다.

그런데 Styled Components를 사용했을 때의 장점은 동적 스타일링이 쉽다는 것이다. 각 컴포넌트에 대한 스타일을 캡슐화하기에 컴포넌트 단위로 자바스크립트의 기능을 활용하여 스타일을 정의하기 때문에 효율적인 스타일 관리가 가능하다.

위의 예시에서 CSS 변수를 활용해 스타일링을 하다보니 JavaScript로 변수를 조작하는 Styled Components의 특징을 활용하는 것이 아쉬웠다. props를 기반으로 동적인 스타일링도 JavaScript 객체와 JavaScript 문법을 사용할 수 있어 이를 더 잘 활용하는 방법이라는 생각이 들었다.

➡ ThemeProvider를 이용한 스타일 변수를 사용하는 방식으로 변경하기로 했다.

ThemeProvider를 이용한 변수 사용

Styled Components 의 ThemeProvider는 React 컴포넌트 전체에 테마를 제공한다. 테마는 색상, 폰트 크기, 여백 등의 스타일 속성을 포함할 수 있으며, 이러한 스타일 속성들을 테마 객체에 저장하여 사용한다.

애플리케이션의 스타일링을 한 곳에서 관리하는 테마 객체를 만들 수 있다. 이를 통해 색상, 폰트, 간격 등의 디자인이나 테마가 변경되어야 할 때, 테마 객체의 값 하나를 변경함으로써 전체 애플리케이션에 스타일을 업데이트한다.

이후에 다크 모드와 라이트 모드를 구현할 때에도 테마를 쉽게 전환할 수 있다.

1. 테마 객체 정의 + 테마 인터페이스 정의

애플리케이션에서 사용할 스타일 속성을 객체 형태로 정의한다.

// theme.ts
export const theme = {
  maxWidth: '1800px',
  sidebarWidth: '300px',
  headerHeight: '56px',
  footerHeight: '56px',
  layoutPadding: '1rem',
  textColor: '#111111',
  textHover: '#eee',
  categoryText: '#7A7C85',
  categoryTextActive: '#18191B',
  categoryBg: 'rgba(207,210,226,0.2)',
  categoryBgHover: 'rgba(207,210,226,0.2)',
};

TypeScript를 사용하여 테마에 포함될 속성의 타입을 정의한다. 테마 객체의 속성을 TypeScript에게 알려주어 자동완성으로 빠르게 개발할 수 있다. 게다가 테마 객체에 대한 타입을 정의하여 오타 등으로 발생하는 오류를 사전에 찾아내 타입 안정성을 높인다.

// styled.d.ts
import 'styled-components';

declare module 'styled-components' {
  export interface DefaultTheme {
    maxWidth: string;
    sidebarWidth: string;
    headerHeight: string;
    footerHeight: string;
    layoutPadding: string;
    textColor: string;
    textHoverColor: string;
    navTextColor: string;
    navTextActiveColor: string;
    navTextHoverColor: string;
    navBgHoverColor: string;
    navBgActiveColor: string;
    navBgActiveHoverColor: string;
  }
}

2. ThemeProvider 사용

생성한 테마 객체를 ThemeProvider로 전달하여 애플리케이션 전역에서 사용한다. ThemeProvider 컴포넌트는 정의된 테마를 컴포넌트 트리에 주입하는 역할이다. 이를 통해 하위 컴포넌트 어디서든지 테마에 접근할 수 있다. 최상위 컴포넌트에 하위 컴포넌트를 감싸 사용한다.

// Root.tsx
...
export default function Root() {
...
  return (
    <ThemeProvider theme={theme}>
      <StLayoutContainer>
        <AuthContextProvider>
          <Header />
          <Sidebar />
          <Main>
            <Outlet />
          </Main>
          <Footer />
          <ToastContainer />
          <GlobalStyle />
        </AuthContextProvider>
      </StLayoutContainer>
    </ThemeProvider>
  );
}

3. 스타일 적용하기

ThemeProvider의 하위 컴포넌트에서는 props를 통해 theme 객체에 접근할 수 있다.

${(props) => props.theme.headerHeight}

하지만 코드가 길어져 보기에 좋지 않고 코드를 읽기 위해 소요되는 시간이 늘어난다. 게다가 작성해야 하는 코드양도 많아져 유지보수하기 불편하다.

theme 객체를 구조분해 할당하여 theme 객체를 매개변수로 받아 테마 객체에 정의된 값을 사용했다.

// Main.tsx
import { PropsWithChildren } from 'react';
import styled from 'styled-components';

export default function Main({ children }: PropsWithChildren) {
  return <StMain>{children}</StMain>;
}

const StMain = styled.main`
  padding: 0 1rem ${({ theme }) => theme.footerHeight} 1rem;
  grid-column: 2;
  grid-row: 2 / -1;
  min-height: calc(100vh - ${({ theme }) => theme.headerHeight});
`;

카테고리:

업데이트:

댓글남기기