모드에 따라 로고 다르게 렌더링 하기

모드에 따라 로고 다르게 렌더링 하기

styled-components as prop을 사용해 모드에 따라 로고를 다르게 렌더링 해보자

·

5 min read

들어가며

이번에는 로고 컴포넌트를 만들었다. 원래는 필요한 곳에 로고 이미지를 삽입해서 사용하거나 Banner 컴포넌트를 사용했는데 아무래도 로고다 보니 재사용성을 고려해 만드는 게 좋겠다는 생각이 들었다. 기존 방식의 문제점과 개선 방향, 그리고 이 과정에서 styled-components의 as prop은 어떻게 사용했는지 풀어볼 것이다.


배경설명

로고는 두 종류가 있다. 밝은 배경에는 다크 버전을, 어두운 배경에는 라이트 버전을 사용한다. 그리고 로고는 홈 헤더, 회원가입, 로그인 페이지에 등장한다.


로고의 특징

본격적으로 시작하기 전에 먼저 로고의 특징을 간단히 짚어보자.

로고는 브랜드를 식별하고 홍보하는 중요한 시각적 요소다. 그렇기 때문에 일관성이 중요하고 여러 곳에서 재사용 될 가능성이 높다. 따라서 어떤 사이즈에서든 깨지거나 비율이 틀어지지 않는 게 좋다. 사이트를 방문했을 때 로고가 깨져 보이면 깔끔하지 않은 인상을 줄 수 있어서 사용자 인식에도 어느 정도 영향을 미친다고 생각한다.


문제점

사실 지금은 큰 문제가 없다. 사용하는 곳이 세 군데밖에 없기도 하고 중단된 프로젝트라(🥲) 더 이상 요구사항이 추가되지 않아서 변경될 가능성도 없다. 하지만 계속 유지보수 하고 요구사항이 추가된다는 가정하에 바라보면 문제가 보인다.

문제 1: 컴포넌트를 충분히 설명하지 못하는 이름

Banner는 웹사이트에 가로로 긴 직사각형 모양으로 게시되는 광고 또는 현수막을 의미한다. 하지만 로고를 렌더링한다. 그래서 Banner는 적절하지 못한 이름이라고 판단했다. 이름만 보면 로고가 아닌 배너를 렌더링한다고 착각할 수 있기 때문이다.

또 메인 페이지에는 동일한 이름의 컴포넌트가 정의되어 있다. 해당 컴포넌트는 아래 사진처럼 서비스의 의견을 받는 링크를 렌더링한다.

문제는 vscode에서 ‘Banner’를 검색했을 때인데, 전혀 다른 JSX를 렌더링하는 동일한 이름의 컴포넌트가 검색어에 걸린다. 그러면 순간적으로 Banner 컴포넌트가 메인 페이지에서도 사용하고 있다고 착각할 수 있다. 실제로 (로고를 렌더링하는)Banner 컴포넌트가 사용된 곳을 찾으려고 vscode에 검색하다가 메인 페이지에서도 사용하고 있다고 착각한 적이 몇 번 있었다.

기존 컴포넌트보다 위 사진의 UI를 렌더링하는 컴포넌트가 Banner라는 의미에 좀 더 가깝지 않나 싶다.

문제 2: 컴포넌트의 위치

로고는 특성상 여러 페이지에서 사용될 수 있고, 지금도 메인, 회원가입, 로그인 페이지에서 사용하고 있다. 기존 컴포넌트는 회원가입, 로그인에서만 사용한다고 가정한 터라 Components/SignUp 폴더에 있다. 로고는 여러 페이지에서 사용될 수 있기 때문에 SignUp 폴더 바깥에 두는 게 나은 것 같다.

문제 3: 유연하지 못한 스타일

기존 컴포넌트는 컴포넌트 내부에서 margin으로 로고 위, 아래의 간격을 조정하고 있다. 재사용성 측면에서 보면 유연한 방식은 아니다. 현재 회원가입, 로그인 페이지를 기준으로 margin 값을 지정하고 있다. 만약 다른 페이지에서도 로고가 필요하다면, 그런데 주변 간격이 다르다면? 바로 재사용 하기 힘들다.

이런 측면에서 볼 때 로고 컴포넌트에서 주변 간격을 조정하는 CSS 코드는 제외하는 게 여기저기서 가져다 쓰기 좋다고 생각한다.


개선 방향

로고의 특징과 문제점을 바탕으로 개선 방향을 잡았다.

  1. 컴포넌트의 기능을 드러내는 명확한 이름 짓기

  2. 여러 페이지에서 사용한다는 점을 고려해 컴포넌트의 위치 변경

  3. 재사용성을 고려한 스타일 적용

  4. 어떤 사이즈에서도 깨지지 않게 SVG 확장자 사용


1차 변경: 개선점 반영하기

import { styled } from 'styled-components';

import { ReactComponent as LogoLight } from '../image/logo/logo-light.svg';
import { ReactComponent as LogoDark } from '../image/logo/logo-dark.svg';

interface LogoProps {
  mode: 'light' | 'dark';
  width: number;
}

export default function Logo({ mode, width }: LogoProps) {
  return mode === 'light' ? (
    <LogoLightS width={`${width}rem`} height='100%' />
  ) : (
    <LogoDarkS width={`${width}rem`} height='100%' />
  );
}

const LogoLightS = styled(LogoLight)`
  display: block;
`;

const LogoDarkS = styled(LogoDark)`
  display: block;
`;

개선 방향을 기반으로 로고 컴포넌트를 만들었다.

mode, width prop

로고의 모드와 가로 사이즈를 정한다. 세로 사이즈는 가로 사이즈에 맞춰 자동으로 조정되도록 했다.

width 이름은 원래 size였는데 로고의 사이즈가 정해진 게 아니다 보니 헷갈릴 수 있을 것 같다. 그래서 가로 사이즈만 받는다는 것을 드러내려고 이름을 width로 바꿨다. 뭔가 width가 있으면 height도 있을 것 같아서 두 개를 같이 받을까…도 생각해 봤지만 둘 다 받으면 로고의 비율이 달라지는 실수가 생길 수 있어서 height는 제외했다.

참고: SVG에서 인라인으로 height 값을 auto로 하면 콘솔에서 에러가 발생한다. 대신 100%로 하면 동일한 효과를 얻을 수 있음

파일 위치 이동

여러 페이지에서 사용하니 SignUp 폴더 바깥으로 이동했다.

로고 파일 확장자 변경

어떤 사이즈에서도 깨지지 않도록 하기 위해 png에서 svg로 변경했다.

display inline → block으로 변경

로고 아래에 생기는 작은 여백을 제거하고 다른 곳에서 로고의 위치 조정을 편하게 하기 위해 display를 inline에서 block으로 변경했다.

로고 주변에 있는 margin 삭제

재사용성을 높이기 위해 회원가입, 로그인 페이지를 기준으로 잡힌 margin을 제거했다.

여기서 마무리 지어도 기능상 문제는 없다. 하지만 동일한 코드가 반복되는 게 마음에 들지 않아 조금 더 개선하기로 했다.


2차 변경: styled-components as prop을 사용해서 중복 코드 줄이기

import { styled } from 'styled-components';

import { ReactComponent as LogoLight } from '../image/logo/logo-light.svg';
import { ReactComponent as LogoDark } from '../image/logo/logo-dark.svg';

interface LogoProps {
  mode: 'light' | 'dark';
  /** width는 rem 단위 입니다. */
  width: number;
}

export default function Logo({ mode, width }: LogoProps) {
  const LogoComponent = mode === 'light' ? LogoLight : LogoDark;

  return <LogoS as={LogoComponent} width={`${width}rem`} height='100%' />;
}

const LogoS = styled.svg`
  display: block;
`;

동일한 코드 제거를 목표로 개선했다. 로고 컴포넌트의 mode와 width를 제외한 나머지는 전부 똑같다. 그래서 동일한 스타일을 가지는 LogoS 스타일 컴포넌트를 반환하되 as prop을 사용해서 mode에 따라 다른 로고가 렌더링 되도록 만들었다.

styled-components as prop

여기서 잠깐 as prop에 대해 알아보자. as는 styled-components에서 지원하는 prop이다. 공식 문서 소제목만 봐도 알겠지만 다형성을 위한 용도다. 다형성이란 상황에 따라 여러 형태를 가질 수 있는 성질을 말한다. 프로그래밍에서는 어떤 객체의 속성 또는 기능이 상황에 따라 여러 형태로 바뀌는 걸 의미한다.

styled-components의 as prop도 이런 다형성을 지원하는 용도로 사용하는데, 동일한 스타일을 유지하되 최종 렌더링 되는 요소는 다르게 하고 싶을 때 유용하다.

예를 들어 div와 button 요소에 똑같은 스타일을 적용하고 싶다고 해보자. 요소만 다른 스타일 컴포넌트 두 개를 만드는 것 보다 아래처럼 as prop을 사용해서 만들면 하나의 컴포넌트를 재사용할 수 있고 유지보수도 편하다.

const Component = styled.div`
  color: red;
`;

render(
  <Component
    as="button"
    onClick={() => alert('It works!')}
  >
    Hello World!
  </Component>
)

또 다른 실용적인 예시로는 내비게이션 바를 만들 때 유용하다. 내비게이션 바의 일부는 링크고 일부는 버튼이어야 하는데 전부 동일한 스타일이 적용되어야 할 때 as prop을 사용해서 하나의 스타일을 재사용할 수 있다. 이렇게 하면 편하기도 하지만 추후 스타일이 변경되었을 때 수정하기도 쉽다.

로고 컴포넌트를 만들 때는 as prop에 mode에 따라 다른 SVG 컴포넌트를 전달했다. 이렇게 하면 렌더링 되는 컴포넌트만 다르고 스타일은 똑같다..!


로고 컴포넌트 사용하기

사용할때는 아래처럼 사용한다. 디자인 시안에 맞게 mode와 width를 전달한다.

// 라이트 모드
<Logo mode='light' width={8.125} />

// 다크 모드
<Logo mode='dark' width={8.125} />

번외: 주석으로 단위 설명하기

사용하려고 보니 width 값을 전달할 때 단위 정보는 드러나지 않는다. 팀 규칙이라 컴포넌트 내부에 rem을 적용했는데, 만약 어떤 단위를 사용해야 하는지 헷갈린다면 컴포넌트 내부를 봐야 알 수 있어서 주석을 달았다.

interface LogoProps {
  mode: 'light' | 'dark';
  /** width는 rem 단위 입니다. */
  width: number;
}

이렇게 주석을 달아두면 사용하는 곳에서 width prop에 커서를 올렸을 때 아래처럼 툴팁에 관련 설명을 보여줄 수 있다.


마무리

styled-components의 as prop은 처음 사용했는데 굉장히 유용했다. 예전에 문서 보다가 과연 언제 쓰려나 했는데 이번에 써봐서 흥미로웠다,, 새로 알게 된 건 html 요소만 전달 해야 하는 줄 알았는데 컴포넌트도 전달 할 수 있었다. 생각보다 더 유연하게 사용할 수 있어서 좋은 것 같다.