치춘짱베리굿나이스

[프리온보딩] 220507 내 과제 리팩토링 2 본문

프로젝트/원티드 프리온보딩

[프리온보딩] 220507 내 과제 리팩토링 2

치춘 2022. 5. 8. 01:28

내 과제 리팩토링

공식적인 내생각

컴포넌트 완전 재구축의 현장 (점 점 점 점 점)

div로 떡칠을 하질 않나, 클래스명 없이 nth-child() 선택자로 어떤걸 선택했는지 알아보기도 힘들게 하질 않나, onClick 이벤트를 굳이 div에 걸질 않나, ...

반성을 좀 많이 (...) 하면서 작업 중이다

심지어 어제 작업하면서 린터도 예전에 쓰던 버전 그대로 놔둬서... 새로 교체했더니 아주 시뻘겋게 죽죽

작업 내용

Toggle

토글은 컴포넌트 자체가 단순해서 리팩토링이 그렇게 어렵지 않았다

const handleOnClick = (e) => {
    e.preventDefault();
    setIsSelected(isSelected ? false : true);
}
const handleToggleClick = () => {
    setIsSelected((prevState) => !prevState)
}

필요없는 e.preventDefault() 를 제거해 주었다

isSelected 상태값 세팅은 prevState를 이용하였다

<ToggleWrapper toggle={ifToggle}>
  <div className="selector-parent" onClick={handleOnClick}>
    ...
  </div>
</ToggleWrapper>
<div className={styles.toggleDiv}>
  <button type='button' className={styles.toggleParent} onClick={handleToggleClick}>
    ...
  </button>
</div>

Styled를 전부 제거해주었고, onClick 이벤트가 걸리는 divbutton 태그로 변경하였다

<div className={!ifToggle ? 'selected' : ''}>{firstString}</div>
<div className={ifToggle ? 'selected' : ''}>{secondString}</div>
<div className={cx(styles.toggleElement, { [styles.selected]: !isSelected })}>{firstString}</div>
<div className={cx(styles.toggleElement, { [styles.selected]: isSelected })}>{secondString}</div>

className 지정 시에 삼항연산자 대신 classnames 라이브러리를 이용해 상태값에 따라 추가적인 클래스를 가질 수 있도록 하였다

<div className="back">
  <div></div>
    {/* 
        margin-left: ${props => (props.toggle ? '10rem' : '0')};
    */}
</div>
<div className={styles.toggleBack}>
    <div className={cx(styles.toggleThumb, { [styles.moved]: isSelected })} />
    {/*
        &.moved {
        margin-left: 10rem;
    }
    */}
</div>

아무 내용도 없고 클래스도 없는 div를 덩그러니 놔두는 것보단 클래스명을 지정해주었다

또한 원래는 styled를 사용해서 스타일에 변수를 사용할 수 있었는데, 이제 불가능하므로 isSelected 상태값에 따라 추가적인 클래스를 사용하여 토글 버튼의 좌우 이동을 구현하였다

Tab

<TabWrapper length={selectorArr.length} index={selectedIndex}>
  <div>
        ...
  </div>
  <div>
    ...
  </div>
</TabWrapper>
<div className={styles.tabDiv}>
  <section className={styles.tabTop}>
        ...
  </section>
  <section className={styles.tabBottom}>
        ...
  </section>
</div>

마찬가지로 styled를 제거해주었고, 정체모를 div 두쌍보단 위아래로 나뉘어져 있는게 section을 써도 괜찮겠다 싶어 section 태그로 교체해 보았다

클래스명 없이 nth-child() 에 의지하면 누가 임의로 요소를 끼워넣었을 때 스타일이 전부 망가질 수 있다는 우려에 따라 거의 모든 요소에 클래스명을 달아주었다

심지어 다른 scss 파일에서 한 태그 (예를 들면 button) 스타일을 건들면 페이지의 모든 컴포넌트가 영향을 받아버려서 왠만하면 그냥 클래스명을 다 달아줬다

버튼 스타일 적용한게 다 어그러져서 깜짝 놀랐다;; 스타일드에 너무 익숙해진 탓인가보다

{selectorArr.map((v, i) => (
      <SelectorDiv
        className={selectedIndex === i ? 'selected' : ''}
        key={i}
        onClick={e => handleOnClick(e, i)}
        length={selectorArr.length}
      >
        {selectorArr[i]}
      </SelectorDiv>
    )
    )
}
{selectorArr.map((v, i) => (
    <button
      type='button'
      className={cx(styles.selectorDiv, { [styles.selectedDiv]: selectedIndex === i })}
      key={`selector-arr-${v}`}
      onClick={() => handleOnClick(i)}
      style={{ width: `calc(24rem / ${selectorLength})` }}
    >
      {selectorArr[i]}
    </button>
    )
    )
}

onClick 이벤트 핸들러를 달기 위해 각 탭을 div가 아닌 button으로 바꿔 주었다

버튼은 항상 기본 스타일이 적용되는 게 안 예뻐서 손이 잘 안 갔는데 border 속성만 none으로 지워줘도 div랑 비슷하게 스타일이 적용돼서 좋음

key prop으론 i를 아예 사용 안 했다

onClick 핸들러는 인덱스 i를 반드시 사용해야 하기 때문에 (무슨 탭을 선택했는지 알기 위해서) 부득이하게 화살표 함수를 사용하였다

마찬가지로 삼항연산자로 클래스명을 설정하던 것을 classnames 라이브러리로 바꿔버렸다

인라인 스타일은 쓰고싶지 않았지만 탭의 개수 (selectorLength) 가 변동될 여지가 있음을 고려하여 탭 각각의 폭과 아래의 슬라이더만 인라인으로 맞춰주었다

Slider

수정할 것 투성이였다...

for (let i = 0; i <= 4; i++) {
    ...
      <div
            className="pos-indicator"
            onClick={e => handleOnClick(e, i * 25)}
        >
            <div>{i * 25}%</div>
        </div>
    ...
const SLIDER_VALUES = [1, 25, 50, 75, 100]
const SLIDER_CLASSNAMES = [styles.slider1P, styles.slider25P, styles.slider50P, styles.slider75P, styles.slider100P]

...

for (let i = 0; i < 5; i += 1) {
    ...
        <li
          key={`slider-value-${SLIDER_VALUES[i]}`}
          className={cx(styles.sliderBackElem, SLIDER_CLASSNAMES[i], {
            [styles.sliderFilled]: sliderValue >= SLIDER_VALUES[i],
          })}
        >
    ...
          <button type='button' className={styles.sliderIndicator} onClick={() => handleButtonClick(SLIDER_VALUES[i])}>
            {SLIDER_VALUES[i]}%
          </button>
    ...

일단 슬라이더 시작점이 0이 아니라 1인 것부터 간과해버려서............ 아예 SLIDER_VALUESSLIDER_CLASSNAMES 배열 상수를 선언해놨다

클래스명 배열도 선언한 이유는 styled 컴포넌트에 변수 넘겨서 사용하는 꼼수를 못 사용하기 때문이지! 하하!

슬라이더 뒷부분 눈금과 버튼의 픽셀 매치를 위해서는 각 숫자별로 margin을 다르게 적용해야 해서 styled를 사용할 땐 그냥 단일 클래스 내에서 calc() 를 통해 margin 너비를 계산해주었는데, 그걸 사용하지 못하니 어쩔 수 없이 클래스를 5종류로 분리했다

<SliderWrapper>
    <div className="slider-head">
        <div>{sliderValue}</div>
        <div>%</div>
    </div>
    <SliderBody sliderValue={sliderValue} setSliderValue={setSliderValue} />
</SliderWrapper>
<div className={styles.sliderDiv}>
  <section className={styles.sliderHead}>
    <span className={styles.sliderValue}>{sliderValue}</span>
    <span className={styles.sliderPercent}>%</span>
  </section>
  <section className={styles.sliderBody}>
        ...
  </section>
</div>

div 범벅에서 탈출하기 위해 이번에도 슬라이더가 위 (숫자 나오는 부분) 와 아래 (슬라이더 본체) 로 나뉘어지는 것을 이용하여 section 태그를 사용해 보았다

main이나 aside, header, footer 태그를 사용하기엔 쪼끔 아쉬운 부분이 있다

문자열이 들어가는 부분은 div 대신 span으로 해결하였고, 모든 태그에 클래스를 달아주었다

<input
    className={styles.sliderInput}
    type='range'
  value={sliderValue}
  min='1'
  max='100'
  onChange={handleSliderChange}
  style={{
        background: `linear-gradient(to right, #699092 0%, #699092 ${sliderValue}%, #dddddd ${sliderValue}%, #dddddd 100%)`,
  }}
/>

input 태그의 min값을 1로 설정하여 1 이하로 떨어지지 않게 해주었다

또한 여기에도 인라인 스타일이 사용되었는데, 슬라이더에 색상이 채워지는 효과를 위해 linear-gradient를 사용하려면 현재 슬라이더의 값 (sliderValue) 이 필요하기 때문이다

이런 경우에는 어쩔 수 없는 것 같다 어차피 움직일 때마다 리렌더링 되어야하니,,

<SliderBackgroundWrapper key={i} curValue={sliderValue} indiValue={i}>
    <div className="pos-dot">
        <div></div>
    </div>
    <div
        className="pos-indicator"
        onClick={e => handleOnClick(e, i * 25)}
    >
        <div>{i * 25}%</div>
    </div>
</SliderBackgroundWrapper>,
<li
  key={`slider-value-${SLIDER_VALUES[i]}`}
  className={cx(styles.sliderBackElem, SLIDER_CLASSNAMES[i], {
    [styles.sliderFilled]: sliderValue >= SLIDER_VALUES[i],
  })}
>
  <div className={styles.sliderDot}>
    <CircleIcon />
  </div>
  <button type='button' className={styles.sliderIndicator} onClick={() => handleButtonClick(SLIDER_VALUES[i])}>
    {SLIDER_VALUES[i]}%
  </button>
</li> 

여러 요소를 한 줄로 나열시켜야 한다는 점에 착안하여 div 대신 li를 사용하였고, divonClick 이벤트를 거는 대신 button을 사용하였다

위에서 선언한 SLIDER_CLASSNAMESSLIDER_VALUES 배열을 이용하여 %값마다 서로 다른 클래스명을 가지도록 하였고, 슬라이더가 채워지는 효과를 눈금에도 적용하기 위해 classnames를 통해 조건부 클래스명을 지정하였다

눈금을 만들기 위해 빈 div를 굳이 border-radius를 통해 동그랗게 만들기보단 그냥 있는 SVG를 사용하자 싶어 CircleIcon svg를 불러와 사용하였다

margin-left: ${props => (0.9 - 0.5) * props.indiValue}rem;
&.slider1P {
  .sliderDot,
    .sliderIndicator {
    margin-left: -2.4rem;
  }
}

&.slider25P {
  .sliderDot,
    .sliderIndicator {
    margin-left: -1.2rem;
  }
}

...

styled의 장점 중 하나인 변수 들고오기를 못 사용해서 아쉬울 따름이다

각 기준 %값 (1, 25, 50, 75, 100) 별로 클래스명을 다르게 지정하였고, margin을 다르게 주었다

sliderDotsliderIndicatorjustify-contentalign-items를 통해 정확히 위아래에 위치하므로, 같은 margin 간격을 적용해 주었다

Input

<InputWrapper>
    <EmailInput
        ...
  />
  <PasswordInput
      ...
  />
</InputWrapper>
<div className={styles.inputDiv}>
    <section className={cx(styles.inputContainer, styles.inputTop)}>
        ...
  </section>
  <section className={styles.inputContainer}>
        ...
  </section>
</div>

이메일 섹션과 비밀번호 섹션으로 분리해 주었다

inputTop 클래스는 margin-bottom을 적용하기 위한 클래스이다

<label style={{ display: `${ifShown ? '' : 'none'}` }}>
    Invalid E-mail address.
</label>
<svg>
    ...
</svg>
<label htmlFor='email' className={cx(styles.labelInvalid, { [styles.isHidden]: isHidden })}>
    Invalid E-mail address.
</label>
<CheckIcon className={cx(styles.checkIcon, { [styles.isEmailValid]: isEmailValid })} />

svg는 컴포넌트로 불러오고, 클래스명을 삼항연산자 대신 classnames를 통해 상태값에 따라 다르게 적용하였다

라벨 인라인 스타일도 그냥 추가 클래스로 해결하였다

Dropdown

슬라이더와 마찬가지로 리팩토링마저도 좀 어려웠다

<div className="dropdown-top" onClick={handleOnClick}>
  <div className="dropdown-top-header">{selectedStr}</div>
  <div className="dropdown-top-arrow">
    <svg>
     ...
    </svg>
  </div>
</div>
<button type='button' className={styles.dropdownTop} onClick={handleTopClick}>
    <span className={styles.dropdownTopSpan}>{selectedStr}</span>
    <TopArrowIcon className={styles.dropdownSvgs} />
</button>

div 범벅에서 벗어나 보자

<ul className={styles.selectList}>
  {dropdownArr
    .filter((v) => {
        if (searchInput === '') return v
        if (v.toLowerCase().includes(searchInput)) return v
        return 0
    })
    .map((v) => (
        <li key={`dropdown-list-${v}`}>
          <button type='button' onClick={() => handleListClick(v)}>
              {v}
              </button>
        </li>
    ))}
</ul>

드롭다운을 리스트로 만들었고, startsOf 대신 includes를 사용해 단어가 포함만 되어도 검색되도록 하였다

li 태그는 겉만 감싸는 용도로 사용하고, 내부 버튼은 button 태그를 통해 onClick 이벤트를 적용하였다

다만 handleListClick이 인자로 v를 받는 바람에 무한 리렌더링을 막기 위해 화살표 함수를 사용하였다

useClickAway(ref, () => setIsHidden(true))

react-use 라이브러리의 훅을 사용하여 드롭다운 바깥을 눌렀을 때 숨겨지는 기능도 쉽게 구현하였다

리팩토링 결과물

바로 깃허브 페이지로 deploy해 주었다

생각보다 반영 시간이 짧아서 좋은듯

Comments