TIL 106 - IoC(Inversion of Contorl)과 컴포넌트

김영현·2024년 6월 21일
0

TIL

목록 보기
116/129

제어역전

전통적인 프로그래밍의 flow는 다음과 같다.

const filterSpecial = (arr) => {
  	const temp = [];
	for(const element of arr){
    	if(element === 'special'){
        	temp.push(element);
        }
    }
  	return temp;
}

const arr = [...];
           
const specialElements = filterSpecial(arr);

filterSpecial함수는 특정 기능만 동작한다. 배열을 받아와 special이라는 엘리먼트만 들어간 배열을 반환한다.
이때 special이 아닌 normal엘리먼트만 받고싶다면 어떻게 해야할까? 함수 파라미터를 추가해야할까?

그냥 filter함수를 쓰면된다.

const normalElements = arr.filter((element) => element === 'normal');

이 함수는 어떻게 필터링 할지를 사용자에게 맡기고있다. 즉, 제어가 역전된 것이다.

=> 제어 역전이란 즉, 객체나 메서드의 제어를 외부에 위임하는 원칙이다.


React에서의 제어 역전(Composition)

재사용가능한 컴포넌트는 어떻게 만들 수 있을까? 정말 다양한 방법론이 존재하지만, 공식문서에 나와있는 Composition을 한번 살펴보자.

예를들어 Sidebar, Dialog같은 컴포넌트는 컴포넌트의 하위항목이 어떤게 존재할 지 미리 알 지 못한다.
이때 제어 역전을 사용하여 컴포넌트를 설계하면 조금 더 유연한 컴포넌트가 될 것이다.

코드의 출처 : https://legacy.reactjs.org/docs/composition-vs-inheritance.html

//children을 이용하여 제어를 역전한다.
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

//children으로 내부 값을 넘겨줌으로써 유연한 변경에 대처가 가능해졌다.
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

React에서는 children을 이용하여 제어역전을 행하는 모습을 볼 수 있다.


제어, 비제어 컴포넌트에 IoC를 한 스푼

이전포스팅에서 만든 Accordion컴포넌트의 개선사항을 한 번 떠올려보자.

현재 Accordion컴포넌트는 상태를 내부에서 관리한다. 그렇기에 다수의 Accordion컴포넌트를 묶어서 하나의 상태로 관리하기가 어렵다.
이때 사용 가능한 것이 바로 제어 역전이다.

useControlled.tsx

Accordion컴포넌트는 현재 비제어컴포넌트다.
React 공식문서 정의인 ref를 사용하여 DOM조작을 해서 비제어컴포넌트란 것이 아니라, 외부에서상태를 조작하지 않는다는 의미에서 비제어다.

이걸 제어 방식도 사용할 수 있게 만들면 어떨까? 즉, 외부에서 상태를 주입한다면 그걸 사용하고, 아니라면 내부상태를 사용하는 방식.

비제어-제어 방식을 바꿔주는 로직을 훅으로 한 번 만들어보자.

import { useCallback, useRef, useState } from "react";

interface IUseControlledArgs<T = any> {
  valueProp?: T;
  defaultValue?: T;
}

type IUseControlledReturn<T = any> = [
  T,
  React.Dispatch<React.SetStateAction<T>>
];

export default function useControlled<T = any>(
  args: IUseControlledArgs<T> = {}
): IUseControlledReturn {
  const { valueProp, defaultValue } = args;

  //외부에서 주입된 상태가 존재하면 제어방식이다.
  const { current: isControlled } = useRef(valueProp !== undefined);

  const [state, setState] = useState<T | undefined>(defaultValue);

  const value = isControlled ? valueProp : state; //외부에서 주입된 상태가 없다면, 내부에서 선언한 상태를 내보낸다.

  const setValue: React.Dispatch<React.SetStateAction<T | undefined>> =
    useCallback((newState) => {
      !isControlled && setState(newState);
    }, []);

  return [value, setValue];
}

이렇게 만든 훅을 사용하여 Accordion컴포넌트를 제어방식으로 한 번 바꾸어보자.

//AccordionContext

//외부 상태 expanded와 외부상태setter인 handleToggle을 인자로 추가하였다.
const AccordionContextProvider = ({
  children,
  handleToggle,
  expanded,
  defaultExpanded = false,
}: AccordionContextProviderProps) => {
  const [expandedState, setExpandedState] = useControlled({
    valueProp: expanded,
    defaultValue: defaultExpanded,
  });

  const onToggle = (e: SyntheticEvent, expanded: boolean) => {
    setExpandedState((prev: boolean) => !prev);

    handleToggle?.(e, !expanded);
    if (iconRef.current) {
      const deg = expanded ? "" : "rotate(-90deg)";
      iconRef.current.style.transform = deg;
    }
  };

  return (
    <AccordionContext.Provider
      value={{ expanded: expandedState, onToggle }}
    >
      {children}
    </AccordionContext.Provider>
  );
};

//Accordion.tsx

const Accordion = ({
  children,
  expanded,
  defaultExpanded = false,
  handleToggle,
  ...rest
}: AccordionProps) => {
  return (
    <AccordionContextProvider
      defaultExpanded={defaultExpanded}
      expanded={expanded}
      handleToggle={handleToggle}
    >
      <div className="bg-white shadow-lg pb-1" {...rest}>
        {children}
      </div>
    </AccordionContextProvider>
  );
};

//사용처

const Navigation = () => {
  const [expanded, setExpanded] = useState<string | false>(false);

  const handleToggle =
    (panel: string) => (e: SyntheticEvent, isExpanded: boolean) => {
      setExpanded(isExpanded ? panel : false);
    };
  return (
    <>
      <Accordion
        expanded={expanded === "accordion1"}
        handleToggle={handleToggle("accordion1")}
      >
        <Accordion.Summary>메뉴1</Accordion.Summary>
        {mappedRoutesStyleArray.map(([link, { name }]) => (
          <Accordion.Content key={link}>
            <Link href={link}>{name}</Link>
          </Accordion.Content>
        ))}
      </Accordion>
      <Accordion
        expanded={expanded === "accordion2"}
        handleToggle={handleToggle("accordion2")}
      >
        <Accordion.Summary>메뉴2</Accordion.Summary>
        <Accordion.Content>이동1</Accordion.Content>
        <Accordion.Content>이동2</Accordion.Content>
        <Accordion.Content>이동3</Accordion.Content>
      </Accordion>
  </>
  );
};

useControlled훅을 통해 외부상태를 사용하게 하여 여러 아코디언 메뉴를 관리할 수 있게 되었다.


참고 링크

material-ui : https://github.com/mui/material-ui/blob/next/packages/mui-utils/src/useControlled/useControlled.js
카카오FE 기술블로그 : https://fe-developers.kakaoent.com/2022/221110-ioc-pattern/

profile
모르는 것을 모른다고 하기

0개의 댓글