리액트 컴포넌트의 제어권을 사용하는 쪽에 넘기는 IoC 패턴 다섯 가지를 렌더링 제어와 상태 제어 두 갈래로 나눠 정리한 글이다. Render Props → 합성 컴포넌트, 제어 컴포넌트 → Props Getter → State Reducer 순으로 앞 패턴의 한계가 다음 패턴을 부르는 구조로 이어진다.
핵심 포인트- 요구가 늘 때마다 프로퍼티를 추가하면 종류가 불어나고 복잡도가 커지며, 이미 쓰이는 컴포넌트를 고쳐 사이드이펙트가 생긴다. IoC 는 그 지점을 피하는 선택지다.
- Render Props 는 렌더링 함수를 프로퍼티로 받는다. 리스트·테이블처럼 같은 렌더링이 반복될 때 유용하지만 복잡한 조건에는 한계가 있다.
- 합성 컴포넌트는 Context/Provider 로 여러 컴포넌트가 하나의 로직을 공유하게 해, UI 배치가 바뀌어도 기존 로직을 그대로 쓰면서 대응한다.
- 제어 컴포넌트는 내부 state 대신 외부 프로퍼티와 콜백을 쓰게 하지만, 상태와 콜백이 많아지면 프로퍼티가 폭증한다.
- State Reducer 는 reducer 자체를 프로퍼티로 받아 내부 reducer 와 결합한다. 콜백이 늘거나 바뀌어도 프로퍼티는 reducer 하나뿐이다.
상세 정리- 전제 정리: 컴포넌트는 props 와 state 를 내부 로직으로 가공해 렌더링 결과를 만든다. 사용자는 props 만 넘길 뿐 결과는 내부가 결정한다는 것이 기본 구조다.
- Render Props 의 함정: `render(...)` 처럼 함수 형태로 호출하면 리액트가 컴포넌트로 인식하지 않아 훅 같은 컴포넌트 전용 기능을 쓸 때 주의해야 한다. `<Render />` 컴포넌트 형태 호출과 결과가 다르다.
- 합성 컴포넌트 예시: 가운데 input 과 양쪽 +/− 버튼으로 된 Counter 에서 버튼 위치만 바꾸는 UI 요구가 들어온 상황을 든다.
- 안이한 대응의 문제: `reverse` 같은 프로퍼티를 추가하거나 render 함수를 고치는 방식은 비슷한 요구가 또 오면 계속 복잡해져 사용성을 해친다.
- 합성 컴포넌트 구현: Counter 는 children 을 CounterContext.Provider 로 감싸기만 하고, 각 구성요소가 Context 를 통해 로직을 공유한다.
- 얻는 것: 구성요소별로 분리된 컴포넌트가 Counter 도메인에 종속되지 않아 각자의 역할(SRP)로도 쓰인다.
- 잃는 것도 명시한다. Context/Provider 구성이 추가로 필요하고 각 컴포넌트에 Context 사용 로직이 붙어 복잡도가 오르며, 프로퍼티 대신 써야 할 컴포넌트 수가 늘어 사용성이 나빠질 수 있다.
- 제어 컴포넌트 구현: 프로퍼티가 들어오면 그 값을, 없으면 `useState` 값을 쓰는 커스텀 훅을 만들어 내부 상태와 외부 제어를 한 인터페이스로 다룬다.
- Props Getter 의 동기: 제어 컴포넌트는 상태와 콜백을 같이 넘겨야 하고, 하나만 바꾸고 싶어도 기능이 같은 콜백 전부를 다시 정의해야 한다.
- Props Getter 구현: 상태와 콜백들을 묶은 커스텀 훅이 컴포넌트에 필요한 프로퍼티를 반환하므로, 사용자는 필요한 콜백만 오버라이딩한다.
- 그래도 남는 문제: 콜백 프로퍼티가 많다는 근본 문제는 그대로이고 컴포넌트가 복잡할수록 커진다.
- State Reducer 구현: 컴포넌트가 외부 reducer 를 프로퍼티로 받아 내부 reducer 와 결합한다. 외부 reducer 는 `next` 함수를 호출해 내부 reducer 를 이어 쓸 수 있어, INCREMENT 액션만 바꾸고 나머지는 내부 것을 그대로 쓰는 식이 가능하다.
- 부가 이점: reducer 는 `useState`·`useRef` 같은 훅을 쓰지 않으므로 컴포넌트 밖 별도 파일로 분리할 수 있다.
- 마무리에서 글이 패턴을 점점 보완하는 흐름으로 쓰였을 뿐 어떤 패턴이 무조건 좋다고 말하긴 어렵다고 못 박는다.
왜 읽나공통 컴포넌트에 프로퍼티가 계속 붙어 감당이 안 되는 프론트엔드 개발자에게 제어권을 밖으로 넘기는 다섯 가지 방법과 각각의 대가를 정리해 준다.