전통적인 프로그래밍의 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/