돌멩이 하나/셀프 크리틱

[기능 구현 챌린지] Chart Component

미래에서 온 개발자 2023. 5. 22. 09:12
 

Frontend Mentor | Expenses chart component coding challenge

In this challenge, you'll create a bar chart component from scratch. We provide a local JSON file, so you can add the chart data dynamically if you choose.

www.frontendmentor.io

 

💡 기능 구현 목표:
- Bar 차트를 보고 개별 Bar 위로 마우스를 가져가면 일별 정확한 금액을 볼 수 있습니다.
- 오늘 요일의 Bar는 다른 요일의 Bar와 색상이 다릅니다.
- 기기의 화면 크기에 따라 콘텐츠에 대한 최적의 레이아웃을 작성합니다. (데스크탑/모바일)
- hover에 따라 인터랙티브 요소가 동작합니다.
- **보너스**: 로컬 JSON 파일에 제공된 데이터를 기반으로 동적으로 Bar를 생성합니다.

 

 

구현 화면 이미지

데스크탑 뷰

 

모바일 뷰

 

1. 사용한 기술 스택:

react, styled-components

 

2. 해당 스택을 선택한 이유:

chart.js 라이브러리를 사용해 보고 싶어서 선택한 챌린지였는데, 막상 chart.js를 설치하고 난 다음에 보니 css 커스텀을 하느니 직접 컴포넌트를 만드는 게 더 빠를 것 같아서 방향을 선회했다. 

 

막대 하나를 기본 컴포넌트로 만든 다음 요일별 데이터를 map으로 돌려서 차트를 작성했다. 지출 금액에 따라 Bar 높이를 다르게 표현해줘야 했기 때문에 props로 변수를 받을 수 있는 styled-components를 사용하기로 정했다. 

 

 

핵심 기능 1) 막대의 높이 다르게 표현하기

  const calculateHeight = (amount) => {
    const HEIGHT_MULTIPLIER = 3.6;
    return `${amount * HEIGHT_MULTIPLIER}px`;
  };
  
    return (
        <S.Bar
          height={() => calculateHeight(amount)}
        />
  );
  
  
// styled-components 코드
export const Bar = styled.div`
	height: ${(props) => props.height};
`

3.6을 곱해준 이유는 디자인 시안을 봤을 때 시안에 가장 근접한 그래프가 나오는 숫자였기 때문이었다. 이런 매직 넘버를 쓰는 게 좋은 코드는 아니라는 블로그 포스팅을 바로 며칠 전에 했는데, 알면서도 지키기 어렵다.

 

 

핵심 기능 2) hover 시 지출 금액을 툴팁으로 보여주기

 :before 셀렉터를 사용해 visibility를 초기값 hidden으로 설정한 다음, 막대 그래프에 hover시 visibility의 속성 값을 hidden에서 visible로 바꿔준다. 

 

a. useState를 사용해 visibility 상태를 관리한다. (초기값 "hidden")

b. 막대 그래프에 onMouseEvent와 onMouseLeave 이벤트 핸들러 추가

c. onMouseEnter 이벤트 핸들러에서 visibility 상태를 "visible"로 업데이트하고, onMouseLeave 이벤트 핸들러에서 "hidden"으로 업데이트

d. 툴팁의 visibility 속성을 visibility 상태에 따라 동적으로 설정

 

function Bar({ label, amount }) {
  const [visibility, setVisibility] = useState("hidden");
  const handleMouseEnter = () => {
    setVisibility("visible");
  };
  const handleMouseLeave = () => {
    setVisibility("hidden");
  };

  return (
    <S.Wrapper>
      <S.BarArea>
        <S.Bar
          amount={amount}
          visibility={visibility}
          onMouseEnter={handleMouseEnter}
          onMouseLeave={handleMouseLeave}
        />
      </S.BarArea>
      <S.Label>{label}</S.Label>
    </S.Wrapper>
  );
}

export default Bar;


// styled-components 코드

export const Bar = styled.div`
  position: relative; 
  border-radius: 0.375rem;
  cursor: pointer;
  // 생략

  &:before {
    content: "${(props) => `$${props.amount}`}";
    position: absolute;
    top: -3rem;
    left: -0.5rem;
    width: 4.5rem;
    padding: 0.6rem 0.2rem;
    margin-bottom: 0.4rem;
    background-color: var(--dark-brown);
    color: #fff;
    border-radius: 0.375rem;
    text-align: center;
    visibility: ${(props) => props.visibility};
  }
`;

 

핵심 기능 3) 오늘 요일의 Bar를 다른 색상으로 표현

.getDay() 메소드를 사용하면 일요일은 0, 월요일은 1, ... 토요일은 6을 리턴한다. 해당 값을 json 데이터의 라벨과 매핑하여 isToday 라는 상태를 관리하고, boolean 값을 props로 보낸다.

 

  const [isToday, setIsToday] = useState(false);
  const DAYS = {
    mon: 1,
    tue: 2,
    wed: 3,
    thu: 4,
    fri: 5,
    sat: 6,
    sun: 0,
  };
  useEffect(() => {
    const today = new Date().getDay(); // 4 => thu
    setIsToday(DAYS[label] === today);
  }, [label]);
  
    return (
        <S.Bar
          today={isToday}
        />
  );
  
  // styled-components 코드
  export const Bar = styled.div`
	  background-color: ${(props) =>
	    props.today ? "var(--cyan)" : "var(--soft-red)"};
  `

 

 

의외로 헤맨 부분 

1. styled-components에서 background-image: url() 경로 작성하기

: url() 안에 ${logo} 와 같은 형식으로 작성한다. 

 

폴더 구조

폴더 구조가 다음과 같을 때, 이미지를 import 하고 해당 파일을 url() 안에 ${} 를 사용해 넣어준다.

// MyBalacne/style.js 파일
import styled from "styled-components";
import logo from "../../images/logo.svg";

export const Wrapper = styled.div
  background-image: url(${logo});
  background-repeat: no-repeat;
  background-position: 90% 50%;
`

 

2. 모바일 반응형 width 제어

export const Wrapper = styled.div`
  margin-top: 1rem;
  padding: 2rem;
  width: 60%;
  max-width: 600px;
  min-width: 530px;
  background-color: var(--very-pale-orange);
  border-radius: 1rem;

  @media screen and (max-width: 420px) {
    width: 90vw;
    max-width: none;
    min-width: unset;
  }
`;

 

max-width와 min-width 속성을 초기화시켜주고 싶었다. max-width의 none 키워드는 최대 너비를 정하지 않는 것이다. 반면 min-width에서는 none 값을 지원하지 않고, mdn에 따르면 최소 너비를 정하지 않는 키워드가 auto인데, 리셋되지가 않았다. stack overflow를 뒤져서 unset 이라는 키워드를 찾아냈다. mdn에는 왜 unset 키워드가 없는가.. 미스테리이다. 

 

 

똑같은 챌린지를 같이 해보고 서로의 코드를 공유하는 스터디를 하고 있는데, styled-components 에서 css 라는 helper 함수에 대해 처음 알게 되었다. sass에서 @mixin 으로 자주 쓰는 스타일 코드를 묶어놓고 재사용하는 것과 비슷한 방식으로 활용할 수 있을 것 같다. 

// 선언부
import { css } from "styled-components";

export const container = css`
  width: 500px;
  height: fit-content;
  padding: 20px;
  border-radius: 20px;
`;

// 사용부
import styled from "styled-components";
import { container } from "../../styles/sharedStyles";

export const Container = styled.div`
  ${container}
  // some css styling codes
`;

 

 

* Repository: https://github.com/Ah-ae/chart-component

* Live URL : https://chart-component-practice.vercel.app