이러한 폼에서는 제어 컴포넌트로 구성할지 비제어 컴포넌트로 구성할지에 대한 고민이 필요합니다.
다음의 2가지 Form 형식을 참고하여 제어 컴포넌트와 비제어 컴포넌트가 무엇인지 알아보겠습니다.
제어 컴포넌트
리액트 공식문서에서 제공하는 제어 컴포넌트의 정의
우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
Zustand는 React 기반 프로젝트에서 전역상태를 관리하는 매우 유용한 라이브러리입니다. 가장 대중적으로 사용되던 Redux의 불편함을 뒤로하고, 편리하고 깔끔한 zustand가 npm trends에서 비등비등한 사용량을 보여주고 있습니다. 이에, Zustand가 어떠한 장점을 가지고 있고 사용하는 방법을 알아보는 시간을 가져보겠습니다.
(React CustomHook 및 상태 관리의 기본 개념에 대한 숙지를 전제한 상태로 작성되었습니다)
Redux를 뛰어넘은 zustand
Zustand의 장점
Provider를 설정해줄 필요 없다.
보일러플레이트가 거의 zero에 가깝다.
Redux와 같은 Flux Pattern을 사용하므로 친숙하다.
Redux Devtools를 사용할 수 있다.
Zustand도 다른 상태관리 라이브러리와 마찬가지로, 상태를 구독하고 있는 Listener들을 저장합니다.
이후, 상태가 변경되었을 때 등록된 리스너들에게 상태가 변경되었다고 알려주며, 최신화된 상태값을 전달합니다.
(이를, 발행/구독 모델이라고 합니다)
사용방법
1. 설치하기
npm i zustand
2. Store 생성하기 (Store: 전역 변수)
Typescript에서는 `create<StoreType>` 을 명시해 주어야 합니다. (외 동일)
function App() {
// "select" the needed state and actions, in this case, the firstName value
// and the action updateFirstName
const firstName = usePersonStore((state) => state.firstName)
const updateFirstName = usePersonStore((state) => state.updateFirstName)
// 전체 사용하는 방법
const {firstName, lastName, updateFirstName, updateLastName} = usePersonStore();
return (
<main>
<label>
First name
<input
// Update the "firstName" state
onChange={(e) => updateFirstName(e.currentTarget.value)}
value={firstName}
/>
</label>
<p>
Hello, <strong>{firstName}!</strong>
</p>
</main>
)
}
Tip: 전체 스토어를 구독하지 않고, 필요한 부분만 구독하여 불필요한 렌더링을 최적화해야 한다.
끝입니다. 정말 간단하죠? 여러분의 프로젝트에서 Zustand를 적극 사용해보세요.
Deep Dive
어떤 라이브러리를 도입하기 전에는 코드 한번쯤은 파하쳐봐야 합니다. 겉모습만 보고 판단하면 안됩니다.
프로젝트에 치명적인 오버헤드를 발생시킬 수 있으니까요.
여기서부터는 zustand의 동작 원리에 대해 파헤쳐보는 내용입니다.
1. set함수 (setState)
Zustand 소스코드의 set함수는 다음과 같이 작성되어 있습니다.
현재 상태를 기반으로새로운 상태를 리턴하는 함수혹은, 아예변경하려는 상태 값을 전달받습니다.
상태 변화는 값이 바뀌었을 때만 발생하며, `Object.assign`을 통해 변경됩니다.
// set function
let state; // 스토어 상태
const setState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (nextState !== state) {
const previousState = state;
// 값이 바뀐 경우에만 변화
state = replace ? nextState : Object.assign({}, state, nextState);
listeners.forEach(listener => listener(state, previousState));
}
};
🔎 Object.Assign을 사용하기 때문에 React의 setState에서 바뀌지 않는 기존 값을 보존하는 보일러 플레이트를 줄일 수 있다.
// React에서의 방식을 적용한다면
set((state) => ({ ...state, count: state.count + 1 }))
// Zustand에서는 바뀌지 않는 값은 기존 값이 된다.
updateFirstName: (firstName) => set(() => ({ firstName: firstName }))
🔥 주의 🔥 Object.Assign은 얕은 복사로 Nested Object를 바꿀때는 추가 Action이 필요합니다.
const store = create(set => ({
text: '',
count: 0,
setCount: newCount => set({ count: newCount }),
increment: () => set(state => ({ count: state.count + 1 })),
setText: text => set({ text })
}));
store.subscribe(state => console.log('Something's changed: ', state)); // 어떤 상태가 변경되더라도 로그가 출력됨
store.subscribe(
state => console.log('Count is changed: ', count),
state => state.count
); // count 값이 바뀔 때만 로그가 출력됨
store.subscribe(
state => console.log('Text has been changed: ', text),
state => state.text
); // text 값이 바뀔 때만 로그가 출력됨
store.setText('Changed'); // text 값만 변경
// 결과
// Something's changed: [Object]
// Text has been changed: Changed
이상으로, Zustand 사용방법이었습니다.
사용하기 쉬운 만큼 깃허브 코드도 Deep Dive 해볼만 한것 같습니다. 추후에 기여를 해보고 싶네요.