서론
React에서 컴포넌트를 만드는 것은 간단합니다. 하지만 컴포넌트들 간의 소통이 발생하면서 코드가 복잡해지기 시작합니다.
컴포넌트간 데이터를 전달하는 다양한 방법이 있지만. 상황에 맞게 적절한 방법을 선택하는 것은 개발자의 몫입니다.
이번 포스팅에서는 React에서 컴포넌트 간 통신을 위한 실용적인 방법들을 살펴보겠습니다
1. 부모에서 자식으로 props 전달하기
Props는 부모에서 자식으로 데이터와 함수를 전달하는 가장 직관적인 방법입니다.
<Profile name="John" age={28} />
USE CASE:
- 데이터가 부모에서 시작되는 경우
- 자식이 데이터를 변경할 필요가 없고, 읽기(READ_ONLY)만 하면 되는 경우
부수 효과가 없는 순수 컴포넌트로 동작함으로 결과를 예상하기 쉬운 구조입니다.
2. 콜백 함수 전달하기
자식 컴포넌트가 부모에게 알려야 할 때 함수 prop을 사용할 수 있습니다.
function SearchBox({ onSearch }) {
return <input onChange={e => onSearch(e.target.value)} />;
}
그리고 부모에서:
<SearchBox onSearch={setSearchQuery} />
이렇게 하면 자식이 의도를 표현할 수 있게 하면서도 부모가 제어권을 유지할 수 있습니다.
이 방법을 통해 부모가 데이터를 직접 제어하거나(제어 컴포넌트) 자식의 데이터 처리를 구독할 수 있습니다(비제어 컴포넌트)
3. State Lifting를 통한 상태 관리
두 형제 컴포넌트가 같은 데이터에 접근해야 한다면, 상태를 가장 가까운 공통 부모로 끌어올리는 것이 합리적입니다.
function App() {
const [selectedItem, setSelectedItem] = useState(null);
return (
<>
<ItemList onSelect={setSelectedItem} />
<ItemDetails item={selectedItem} />
</>
);
}
4. 복잡한 트리 구조에서 Context를 이용한 데이터 전달.
여러 컴포넌트가 같은 데이터나 액션에 접근해야 하는데, 하나의 부모 아래 깔끔하게 중첩되어 있지 않다면, React Context를 사용할 수 있습니다.
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [darkMode, setDarkMode] = useState(false);
return (
<ThemeContext.Provider value={{ darkMode, setDarkMode }}>
{children}
</ThemeContext.Provider>
);
}
그러면 어떤 자식이든 다음과 같이 접근할 수 있습니다:
const { darkMode, setDarkMode } = useContext(ThemeContext);
Context가 가장 잘 작동하는 경우:
- 테마
- 인증
- 사용자 기본 설정
- 언어
자주 변하는 값들(폼 입력이나 검색 텍스트 같은)을 context에 넣는 것은 피해야 합니다. 자주 변경되는 값이 정말 context가 맞는지 로컬 레벨에러 처리할 수 없는 데이터인지 검증해봐야 합니다.
5. Ref를 이용해 자식 컴포넌트 컨트롤하기.
때로는 부모가 자식 내부에서 뭔가를 실행시키고 싶을 때가 있습니다 . 이럴 때는 ref를 사용할 수 있습니다.
const InputField = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
reset: () => setValue(''),
}));
return <input value={value} onChange={e => setValue(e.target.value)} />;
});
const inputRef = useRef();
<InputField ref={inputRef} />
<button onClick={() => inputRef.current.reset()}>Reset</button>
이 접근법은 유용하지만, 단방향 데이터 흐름에서 제어의 흐름이 역전되는 방법이기 때문에 데이터 흐름을 파악하는데 어려움이 생길 수 있습니다. 유연성을 위해 제공하지만 이 기능을 필수로 염두한 설계는 지양해야 합니다.
6. 전역 상태 라이브러리
애플리케이션이 커지고 서로 관련 없는 여러 컴포넌트가 공유 데이터와 함께 작업해야 할 때 전역 상태 라이브러리가 도움이 됩니다.
- Zustand
- Redux
- Recoil이나 Jotai
하지만 모든 것을 전역 상태에 넣어서는 안 됩니다. 유지보수 관점에서 데이터는 가능한 지역 레벨로 캡슐화되는 것이 좋습니다. 데이터의 통제를 하는 모듈이 제한될 수록 이해하고 예측하기 쉽기 때문입니다. global 영역에서 접근이 가능하고 변경할 수 있는 만큼 global 상태에 추가하는 것은 신중해야합니다.
7. 횡단 관심사 처리를 위한 커스텀 훅
같은 로직을 반복하고 있다면 커스텀 훅으로 동작을 추상화할 수 있습니다.
function useCart() {
const { items, setItems } = useContext(CartContext);
const addItem = item => setItems(prev => [...prev, item]);
return { items, addItem };
}
이렇게 하면 앱 전체에서 사용되는 로직을 중앙화하면서 컴포넌트를 작고 집중된 상태로 유지할 수 있습니다.
8. 이벤트 버스
이벤트의 발행처와 수신처가 명시적이지 않은 만큼 자칫 코드 복잡도를 높일 수 있느 방법입니다만, 전역 상태로 처리하기 애매하면서 분리된 컴포넌트간의 통신에서 유용할 수 있습니다.
const bus = new EventTarget();
function notifySearch(value) {
bus.dispatchEvent(new CustomEvent("search", { detail: value }));
}
그리고 다른 곳에서 수신:
bus.addEventListener("search", e => {
console.log(e.detail);
});
서로에 대해 알 필요가 없는, 정말로 분리된 모듈이 있을 때 이것을 사용합니다.
마무리
React에서 코드 복잡도는 대부분 데이터 흐름과 연관이 있습니다.
전역 상태나 이벤트 버스같은 방법을 바로 사용하면 안 됩니다. 상태를 끌어올리거나 props를 전달하는 것만으로도 충분하고 가급적이면 지역 레벨에서 데이터를 캡슐화하기 위해 노력해야 합니다.
앱이 커지면서 데이터 전달이 어려운 경우 이런 방법을 쓸 수 있지만 전역적 영향도를 가지는 만큼 신중한 판단이 필요합니다.
가중 중요한 것은 신뢰하고 유지보수할 수 있는 방식으로 직관적인 소통을 하는 컴포넌트들을 만드는 것입니다.