🗳️Code Style
1. Context 사용 방법
use-context-selector 를 사용하여 구현합니다.
custom-hooks 를 만들고, createContextSelector 함수에 hook 을 인자로 전달해 주면, hook 의 return 값에 해당하는 컨텍스트를 사용할 수 있는 useContext hook 과, Provider 를 제공해 줍니다.
import { createContextSelector } from '@/utils/react/create-context-selector';
const useCount = () => {
const [count, setCount] = useState(0)
return { count, setCount };
};
export const {
Provider: CountProvider,
useContext: useCountContext,
} = createContextSelector(useCount);SomeComponent에서 Count 컨텍스트를 사용하려면, 해당 컨텍스트를 활용할 수 있도록 부모 요소에 Provider를 통해 컨텍스트를 전달해야 합니다. 이를 통해 SomeComponent 내부에서 해당 컨텍스트를 활용할 수 있는 상태가 되는 것입니다.
const ParentComponent = () => {
return (
<CountProvider>
<SomeComponent />
</CountProvider>
);
}; 컨텍스트를 사용하는 컴포넌트와 Provider를 한곳에서 관리하고 싶다면 HOC 를 고려해보세요
const SomeComponent = () => {
const count = useCountContext((ctx) => ctx.count);
const setCount = useCountContext((ctx) => ctx.setCount);
return (
<Box>
<Button onClick={() => setCount((cur) => cur + 1)}>{count}</Button>
</Box>
);
};
export default withCountProvider(SomeComponent)function withCountProvider<T extends (props?: any) => JSX.Element | null>(
Component: T,
) {
return function Wrapped(props: PropsOf<T>) {
return (
<CountProvider>
<Component {...props} />
</CountProvider>
);
};
}
export default withCountProvider;selector 함수를 넘겨주어 컨텍스트를 사용합니다.
const SomeComponent = () => {
const count = useCountContext((ctx) => ctx.count);
const setCount = useCountContext((ctx) => ctx.setCount);
return (
<Box>
<Button onClick={() => setCount((cur) => cur + 1)}>{count}</Button>
</Box>
);
};Context를 사용하기 전에 고려해야 할 점
Context는 편리한 동시에 데이터 흐름을 이해하기 어렵게 만들 수 있는 단점이 있습니다. 데이터를 내려주기 위해 Properties를 사용하는 것은 조금 복잡할 수 있지만, 그로 인해 데이터 흐름이 명확하게 드러나는 장점이 있습니다.
만약 state를 공유하는 것이 복잡하거나 prop-drilling 때문에 개발이 어려운 상황이라면, 다음과 같은 대안을 먼저 고려해볼 수 있습니다.
컴포넌트 합성
복잡한 상태를 관리하기 위해 컴포넌트를 조합하고 중첩함으로써, 각 컴포넌트는 자체적인 상태를 가지고 관리할 수 있도록 합니다. 이렇게 하면 상태 관리가 더욱 명확해질 수 있습니다.
상태 지역화
필요한 상태를 가능한 한 해당 컴포넌트 내부로 지역화하여 관리합니다. 이렇게 하면 컴포넌트 간의 상태 공유가 줄어들어 데이터 흐름을 더 명확하게 만들어줍니다.
2. useReducer 로 상태 관리하기
Context는 상태뿐만 아니라 함수도 공유할 수 있는데, 이는 복잡한 상태 관리 로직과 라우터 이동(router.push)과 같은 함수들이 함께 작용할 수 있는 환경을 제공합니다. 하지만 상태와 다른 행동을 하는 로직이 섞이면 코드가 복잡해질 수 있습니다. 이런 경우에는 '관심사 분리'가 필요합니다.
useReducer는 상태 관련 로직을 이전 상태와 페이로드(payload)를 받아 다음 상태를 반환하는 순수 함수입니다. 이로써 컴포넌트 외부에서도 작성할 수 있고, 하나의 액션에서 여러 상태를 조회하고 동시에 수정하는 것이 가능합니다. 이로 인해 상태 관리의 관심사를 분리하고 코드를 단순화할 수 있습니다.
이는 모든 면에서 유리한 것만이 아닙니다. 실제로 작성하는 코드의 양은 useState보다 늘어날 수 있어, 간단한 상태에는 오히려 복잡도가 증가할 수 있습니다. 따라서, 상태가 커지고 복잡해지며 관심사 분리가 필요한 상황에서만 이를 적용하는 것을 권장합니다.
reducers 객체의 각 키 값은 dispatch의 타입(type)으로 사용되며, 해당 키에 해당하는 reducer 함수의 두 번째 매개변수는 payload가 됩니다. 일반적으로 reducer를 사용하려면 원하는 값을 수정한 후 새로운 state를 반환해야 하는데, 이 때 객체 복사와 같은 복잡한 작업이 필요했습니다. 그러나 createSlice 내부에서 Immer 라이브러리를 사용하여 간단하게 할당만으로도 state를 수정할 수 있습니다. 이를 통해 코드를 더 간결하게 작성할 수 있습니다.
따라서 createSlice는 RTK와 비슷한 스타일로 리듀서를 생성하고 관리할 수 있도록 도와주는 함수입니다. 이는 기존 Redux 개발 경험을 보다 편리하게 만들어주며, Immer 라이브러리의 도움으로 불변성 유지를 더욱 간단하게 처리할 수 있게 해줍니다.
import { useReducer } from 'react';
import { createSlice } from '@/utils/react/create-slice';
type GlobalStateType = {
value: number;
};
const initialState: GlobalStateType = {
value: 0,
};
const { reducer } = createSlice({
initialState,
reducers: {
RESET: () => initialState,
INCREASE_VALUE: (state, payload: number) => {
state.value = state.value + payload;
},
DECREASE_VALUE: (state, payload: number) => {
state.value = state.value - payload;
},
},
});
export const useExampleState = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return { state, dispatch };
};
const increase = (value: number) => {
dispatch({
type: 'INCREASE_VALUE',
payload: value,
});
};
const decrease = (value: number) => {
dispatch({
type: 'DECREASE_VALUE',
payload: value,
});
};
위와 같이 사용이 가능하며, reducer 이기 때문에 최후에 연산된 state 로 적용이 가능합니다.
const [value,setValue] = useState(0)
setValue(value + 2);
setValue(value + 3);
// bathcing 으로 인해 value: 3
increase(2);
increase(3);
// 연산을 기억하기 때문에 value: 5Last updated