전통적인 프로그래밍의 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');
이 함수는 어떻게 필터링 할지를 사용자에게 맡기고있다. 즉, 제어가 역전된 것이다.
=> 제어 역전이란 즉, 객체나 메서드의 제어를 외부에 위임하는 원칙이다.
재사용가능한 컴포넌트는 어떻게 만들 수 있을까? 정말 다양한 방법론이 존재하지만, 공식문서에 나와있는 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
을 이용하여 제어역전을 행하는 모습을 볼 수 있다.
이전포스팅에서 만든 Accordion
컴포넌트의 개선사항을 한 번 떠올려보자.
현재 Accordion
컴포넌트는 상태를 내부에서 관리한다. 그렇기에 다수의 Accordion
컴포넌트를 묶어서 하나의 상태로 관리하기가 어렵다.
이때 사용 가능한 것이 바로 제어 역전이다.
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/