관심사분리 (Logic vs View) 2탄

박채윤·2024년 4월 12일
2
post-thumbnail

✨들어가면서...

1편의 마지막 관심사 분리의 시작을 큰 관점에서 본다면 View vs Logic 을 나누눈 것부터 시작이 아닐까해서 이 글을 작성해보았다라는 멘트와 함께 마무리 지었던것이 기억난다.

그렇다면 이번 2편에서는 단순히 View 와 Logic을 나누는 것 뿐만아니라 어떤 방법을 사용해서 나눌건지, 또한 그 이점은 무엇인지에 대해서 알아보고자 한다.

🚗시작

2편을 작성하게된 이유는 dropdown이라는 컴포넌트를 만드는 과정에서 부터 출발한다.

이번주차 과제로 그동안 해보고 싶었던것을 구현해볼 시간이 생겼고 아직 컴포넌트로 만들어보지 않았던 dropdown을 만들게 되었다.

항상 작업해오던 방식으로 다음과 같이 dropdown컴포넌트를 생성했다.

// dropdown.jsx
const Dropdown = ({ items }) => {
  const [isOpen, setIsOpen] = useState(false)
  const [selectedOption, setSelectedOption] = useState("select")
  const boxRef = useRef(null)
  const Arrow = isOpen ? "⬆️" : "⬇️"
  return (
    <DropDownWrapper onBlur={() => setIsOpen(false)} ref={boxRef} {...rest}>
      <DropDownTrigger onClick={() => setIsOpen((prev) => !prev)}>
        <Title>{selectedOption}</Title>
        <div> {Arrow}</div>
      </DropDownTrigger>
      {isOpen && (
        <DropDownItemsWrapper>
          {items.map((item, idx) => (
            <DropDownItems
              isOpen={isOpen}
              key={idx}
              onMouseDown={() => {
                setSelectedOption(item)
                setIsOpen(false)
              }}

              {item}
            </DropDownItems>
          ))}
        </DropDownItemsWrapper>
      )}
    </DropDownWrapper>
  )
}
export default Dropdown
 ... styled-components 를사용한 css code들 ...

위와같이 하나의 dropdown.jsx파일을 생성하고 그 아래에 state 와 event를 관리하는 함수들이 하나로 엮여서 딱봐도 한눈에 들어오지 않는다는 느낌이든다.
개선의 여지가 있을것 같은데 그래서 여러가지 dropdown과 관련된 글들을 검색하기 시작했다.

📃어떤 글

자주사용하게 될 컴포넌트인데 좀더 좋은방법은 없을까? 하는 생각에 관련 글들을 많이 읽어보았다.

그 중 dropdown 을 만드는 글을 읽고난 후 방향을 잡을 수 있었다.
위 글에서는 어떻게 관심사분리를 진행했는지를 순서대로 보여준다. 또한 그렇게 했을때의 효과를 설명해주고 있는데 마침 위에서 작성한 내가처음 만든 dropdown과 같은 코드로 시작을 하고 있어서 나도 이번 글에서 dropdown 을 만드는 글의 진행방식을 차용해서 느낀점과 함께 애기해보려 한다.

✨dropdown 컴포넌트

☝초기의 dropdown

앞서 봤던것처럼 초기 나의 dropdown은 다음과 같은 구조였다.

const Dropdown = ({ items }) => {
   // ... state, event-handlers ... 와같은 다양한 것들
  return (
  // ... 컴포넌트의 view를 구성하는 부분...
}
export default Dropdown
 ... styled-components 를사용한 css code들 ...

지금까지는 이러한 형태를 문제없이 사용해왔는데 여기서 문제가될수 있는부분은 어떤 것일까?

바로 관리 & 확장성 면에서 나의 코드는 좋지 못할 수 있다고 보여진다.

1 관리

View적인 측면에서 우선 생각해보자
하나의 DropDown 컴포넌트로 관리하게 되면 코드를 추가하는 것이 복잡해질 가능성이 있다.
그래서 다음과 같은 방법으로 DropDown컴포넌트를 관리해보자.

 <DropDownWrapper>
      <DropDownTrigger>
        <Title>{selectedOption}</Title>
        <div> {Arrow}</div>
      </DropDownTrigger>
      {isOpen && (
        <DropDownItemsWrapper>
          {items.map((item, idx) => (
            <DropDownItems>{item}</DropDownItems>   
          ))}
        </DropDownItemsWrapper>
      )}
    </DropDownWrapper>
  )
}

dropdown 컴포넌트는 위와같이 trigger가 존재하고 trigger를 클릭할때마다 isopen이라는 상태가 토글되면서 그아래 items라는 컴포넌트가 보여지는 구조이다.

코드의 구성을 보기위해 props를 모두제거한 상태로 작성을했는데 구조가 한눈에 들어오는 이상적인 구조는 아닌것 같다.

위 글에서 제시한 방법에 따라 view를 더 명확하게 보기위해서 trigger.jsx와 items.jsx라는 두가지 컴포넌트로 나누어 보자.

// dropdownTrigger.jsx
const DropDownTrigger = ({ isOpen, selectedOption, clickCallback }) => {
  const Arrow = isOpen ? "⬆️" : "⬇️" // 위아래 화살표모양의 아이콘
  return (
    <S.DropDownTriggerWrapper onClick={() => clickCallback()}>
      <S.Title>{selectedOption}</S.Title>
      <div> {Arrow}</div>
    </S.DropDownTriggerWrapper>
  )
}
export default DropDownTrigger
-------------------------------------------------------------------
// dropdownItems.jsx
const DropdownItems = ({ clickCallback, items }) => {
  return (
    <S.DropDownItemsWrapper>
      {items.map((item, idx) => (
        <S.DropDownItems
          key={idx}
          onMouseDown={() => {
            clickCallback(item)
          }}

          {item}
        </S.DropDownItems>
      ))}
    </S.DropDownItemsWrapper>
  )
}
export default DropdownItems

위와같이 두가지의 더 작은 조각으로 분리하고 두 컴포넌트를 dropdown.jsx에서 불러와보자.

// Dropdown.jsx
const Dropdown = ({items})=>{
   <DropDownWrapper>
     <DropDownTrigger
       isOpen={isOpen}
       clickCallback={onClickTrigger}
       selectedOption={selectedOption}
     />
     {isOpen && <DropdownItems items={items} clickCallback={onClickItems} />}
   </DropDownWrapper>
 )
}
export default Dropdown

view를 명확하게 보기위해서 컴포넌트를 나눴더니 목표로했던 관리적인 측면에서 더 좋아보인다.
만약 추가될 컴포넌트가 생긴다면 더 효과적으로 수정 할 수 있을것 같다.

2 확장성

dropdown컴포넌트이기때문에 프로젝트의 요구사항에 따라 단순 click event뿐만 아니라 keyboard event와같은 다양한 기능들이 추가 될 수가있다.

예시 상황으로 keyBoard event가 추가된다고 생각해보자.

// DropDown.jsx
const Dropdown = ({ items, ...rest }) => {
  const [isOpen, setIsOpen] = useState(false)
  const [selectedOption, setSelectedOption] = useState("select")
  const boxRef = useRef(null)
  const onClickTrigger = () => {
    setIsOpen((prev) => !prev)
  }
  const onClickItems = (item) => {
    setSelectedOption(item)
    setIsOpen(false)
  }
  const keyBoardEvent = (e)=>{
    ... 키보드 이벤트를 정의할 부분 ...
  }
  return (
    <S.DropDownWrapper
      onBlur={(e) => {
        setIsOpen(false)
      }}
      ref={boxRef}
      {...rest}

      <DropDownTrigger
        isOpen={isOpen}
        clickCallback={onClickTrigger}
        selectedOption={selectedOption}
      />
      {isOpen && <DropdownItems items={items} clickCallback={onClickItems} />}
    </S.DropDownWrapper>
  )
}
export default Dropdown

... 키보드 이벤트를 정의할 부분 ...의 코드는 많이 길어질 여지가있다.

DropDown컴포넌트를 더 명확하게 보기위해서 Trigger 와 Items 컴포넌트로 분리해서 파일의 관리 포인트를 줄였는데 위의 예시상황과 같이 새로운 event가 추가된다면
DropDown컴포넌트의 관리포인트 가 많아질 것이고 이는 관리의 어려움으로 이어질 것이다.

그렇다면 어떻게 이 문제점을 해결 할 수 있을까?

✨View 와 Logic의 분리

위의 상황에서 문제된것 그것은 바로 추가되는 logic에대한 처리 일 것이다.
다음 과정에서 logic을 따로관리함으로써 view와 logic을 완전히 분리시켜보자.

🎁Custom Hooks활용

위 글에서 제시한 방법은 Custom hooks를 활용하는 것이다.

// use-drop-down.js
export const useDropDown = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [selectedOption, setSelectedOption] = useState("select")
  const boxRef = useRef(null)
  const onClickTrigger = () => {
    setIsOpen((prev) => !prev)
  }
  const onClickItems = (item) => {
    setSelectedOption(item)
    setIsOpen(false)
  }
  const onClickWrapper = () => {
    setIsOpen(false)
  }

  return {
    isOpen,
    selectedOption,
    boxRef,
    onClickWrapper,
    onClickTrigger,
    onClickItems,
  }
}

위와같이 use-drop-down.js를 생성하고 이를 DropDown.jsx에서 불러와서 구조를 확인하자.

const Dropdown = ({ items }) => {
  const {
    isOpen,
    selectedOption,
    boxRef,
    onClickWrapper,
    onClickTrigger,
    onClickItems,
  } = useDropDown()
  return (
    <S.DropDownWrapper onBlur={onClickWrapper} ref={boxRef}>
      <DropDownTrigger
        isOpen={isOpen}
        clickCallback={onClickTrigger}
        selectedOption={selectedOption}
      />
      {isOpen && <DropdownItems items={items} clickCallback={onClickItems} />}
    </S.DropDownWrapper>
  )
}
export default Dropdown
... css code들 ...

hooks를 적용한 결과물은 다음과 같다. 우리가 Dropdown컴포넌트에서 확인해야할 부분은 바로 view에 대한 부분이다. logic에대한 부분은 hooks로 관리하면서 다음과 같이 깔끔해진 결과물을 눈으로 확인 할 수 있다.

😎분리해야하는 이유

위의 예시코드에서 확인한 것처럼 view와 logic을 분리해서 코드가 한눈에 들어오게 되었다.
이로인해 DropDown 컴포넌트는 유지보수& 재사용 적인 측면에서 이점이 생겼다.

1. 유지보수

지금까지의 작업을 통해서 우린 DropDown컴포넌트를 유지 보수하기 쉽게 만들었다.

  • 만약 view대한 수정이 필요하다면 ? ==> DropDown.jsx 컴포넌트 혹은 하위 컴포넌트를 수정해주면 된다.
  • 만약 logic에대한 수정이 필요하다면 ? ==> use-drop-down.js파일에서 추가,수정,삭제 되는 logic을 추가해주고 간단하게 DropDown컴포넌트에 불러와서 적용시켜주기만 하면된다.

2. 재사용

모든 컴포넌트를 다음과같은 형태로 만드는 것은 비효율적인 과정일 수 있다.
하지만 로그인페이지의 input 혹은 DropDown 같은 컴포넌트는 어떤 프로젝트에서도 사용될 가능성이 있다.

logic을 view에서 분리시킴으로써 이런 컴포넌트들은 어떤 프로젝트에서든 재사용하기 쉬워지게 되었다.

실제 DropDown컴포넌트를 재사용한 예시를 보며 알아보자.

✨적용

간단하게 TodoList를 만드는 프로젝트를 진행하던 중 DropDown을 사용해야했는데
만들어둔 DropDown을 재사용 해봤다. 이번 프로젝트의 css는 tailwindCss를 사용했는데 적용과정을 살펴보자.

//view의 구조
  <S.DropDownWrapper > //전체 dropdown을 감싸는 Wrapper
      <DropDownTrigger // 펼쳐지기전 dropdown의 형태
      />
      {isOpen && <DropdownItems/>} // 펼쳐진 item들의 형태
  </S.DropDownWrapper>
  )
}

logic은 기본동작만 구현해놨기때문에 간단하게 use-drop-down.js를 가져오기만 하면된다.
dropdown은 다음과 같은 구조를 가지기 때문에 프로젝트의 요구사항에 맞게 뼈대에 옷만 입혀주는 과정을 해주면 된다 다음과 같이 말이다.

<div
      className="relative w-[20rem] cursor-pointer"
      onBlur={onClickWrapper}
      ref={boxRef}>
      <DropDownTrigger
        isOpen={isOpen}
        clickCallback={onClickTrigger}
        selectedOption={urlParams.get('option')}
      />
      {isOpen && <DropdownItems items={items} clickCallback={onClickItems} />}
    </div>

같은 과정이기때문에 예시로 하나의 예시코드만 불러왔지만 trigger와 items에도 동일한 작업을 해주면 다음과 같이 잘 동작되는 것을 볼 수 있다.

🙌마치며...

이번 작업을 진행하면서 view 와 logic을 분리하는 과정은 좀 귀찮은 작업일 수 있지만 후에 얻을 이점을 생각한다면 꼭 필요한 작업이 아닌가 하는 생각이 든다.

지금까지 작업한 과정을통해 dropdown 컴포넌트는 headless component가 되었는데.
view와 logic을 분리하는데 가장 큰 역할 을 한것은 hooks라는생각이 든다.

사실 지금까지 hooks를 왜사용하는지 잘 이해하지 못했엇다. 그저 상태를 포함한 함수? 라고 만 생각이 들었엇는데 일련의 과정을 통해 hooks의 장점을 이해하게 되었고 custom hooks를 사용하는것이 view와 logic을 분리하는데 결정적인 역할을 했다는 생각이 든다. 또한 logic 과 view를 나눈다는 것이 확장성, 관리 측면에서 어떤 이점을 가져다주는지 생각해 볼 수 있엇던것 같다.

참고자료👇

(번역) 헤드리스 컴포넌트: 리액트 UI를 합성하기 위한 패턴

profile
왕이될 상인가

0개의 댓글