관심사의 분리에 따라 Store 를 사용해 상태관리를 해보자.
상태를 React 안에서 관리하지 않고, 외부에서 External Store 라는 형태로 관리하는 법에 대해 알아보자.
separation of concerns, 줄여서 Soc 라고 부른다.
하나의 프로그램은 작은 부품이 모여서 만들어진다. 우리는 이미 작은 컴포넌트를 합쳐서 더 큰 컴포넌트를 만드는 방식으로 개발하고 있다.
예를 들어, 다음과 같이 나타낼 수 있다. 이는 기능
을 위주로 나누었다.
이런 식으로 나누는 이유는 무엇일까?
서로의 관심사
가 다르다.
예를 들어, App
에서는 TextField
가 어떻게 동작하는지 알 필요가 없다.
각 부분은 하나의 역할, 하나의 관심사로 격리 됨으로써, 복잡도를 낮추게 된다.
위에서 처럼 기능
관점에서 볼 수도 있고, 설계
관점, 프로세스
관점에서도 볼 수 있다.
사용자에서 가까운 것과 먼 것으로 구분한다.
예를 들어,
가장 가까운 건 UI 를 다루는 부분,
그 다음에는 Business 를 다루는 부분,
그 너머에는 데이터에 접근하고 저장하는 부분으로 나눌 수 있다.
Input → Process → Output
거대한 프로그램이 아니라고 해도, 흔히 이렇게 3단계로 코드를 적절히 구분만 하면,
코드를 이해하고 유지보수하는데 크게 도움이 된다.
하나의 Output은 다시 사용자에게 Input을 요청하게 되고, 일반적인 프로그램은 다음과 같이 계속 순환하는 구조가 된다.
만약 Input Process 가 묶여 있는 구조라고 생각해보자. Process 부분을 테스트 하고 싶은데, I/O 가 섞여 있으면 복잡해진다. Process 부분이 따로 나와 있으면, 단위 테스트를 할 수 있어서 테스트 하기가 쉬워진다.
(물론 E2E 테스트를 배웠기 때문에 I/O 테스트도 가능하다.)
MVC 에 대한 대안이다.
단방향 데이터 Flow 를 강조한다.
Action
Dispatcher
Store
(여러 개)View
이것을 배경으로 하는 Redux는 단일 Store를 사용함으로써
좀 더 단순한 그림을 제안한다.
Action
Store
View
여기서 State를 변경한다
의 의미는
기존의 State 를 고치지 않고, 새로운 State 를 만든다는 것이다.
const state = { name: 'tester' }
→ state.name = 'New Name' (X)
→ const nextState = { ...state, name: 'New Name' } (O)
3단계를 거칠게 매칭해보자.
지금까지는 상태를 useState 같은 Hook 을 이용해서 처리했다. 이제 다르게 처리해보자.
먼저 External Store 의 뜻은 무엇일까?
External: React
의 바깥 부분을 의미한다.
→ External Store: Store 가 React 의 안에 있지 않다 를 의미한다.
Architecture 관점에서 말하는
바깥 부분
이 아니다.
따지고 보면 가장 바깥 부분은 UI 이다. 즉, React 가 바깥 부분에 있다.
여기서는 말하는바깥 부분
은 React 입장에서바깥 부분
을 말한 것이다.
Increase 버튼을 누르면 count 가 1씩 증가한다. 화면에도 증가되는 상태 값이 보인다.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
다음과 같이 작성하면 상태가 바뀌어도 화면에 적용되지 않는 문제가 발생한다.
import { useState } from 'react';
let count = 0;
export default function Counter() {
const handleClick = () => {
count += 1;
console.log(count);
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
Class 컴포넌트를 쓰던 시절에는 이렇게 사용했다. 강제로 리렌더링을 했다.
import { useReducer } from 'react';
let count = 0;
export default function Counter() {
const [ignored, forceUpdate] = useReducer((x) => x + 1, 0);
const handleClick = () => {
count += 1;
// 강제로 렌더링
forceUpdate();
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
Custom Hook 으로 분리했더니, 상태를 React 가 관리하지 않게 됐다.
이런 식으로 만드는게 External Store 의 기본적인 아이디어이다.
import { useState } from 'react';
let count = 0;
function useForceUpdate() {
const [state, setState] = useState(0);
const forceUpdate = () => {
setState(state + 1);
};
return forceUpdate;
}
export default function Counter() {
const forceUpdate = useForceUpdate();
const handleClick = () => {
count += 1;
forceUpdate();
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
함수를 항상 같게 만들 수 있다.
왜 함수를 같게 만들어야 할까?
나중에 useEffect(무언가를 처리, [forceUpdate]);
를 해주기 위해서이다.
forceUpdate 가 바뀌가 되었을 때 무언가를 처리해주기 위해서이다.
// hooks/useForceUpdate.ts
import { useState, useCallback } from 'react';
export default function useforceUpdate() {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
}
// components/Counter.tsx
import useForceUpdate from '../hooks/useForceUpdate';
let count = 0;
export default function Counter() {
const forceUpdate = useForceUpdate();
const handleClick = () => {
count += 1;
forceUpdate();
};
return (
<div>
<p>{count}</p>
<button type="button" onClick={handleClick}>
Increase
</button>
</div>
);
}
이런 접근을 잘 하면, React 가 UI 를 담당하고, 순수한 TypeScript (또는 JavaScript) 가 비즈니스 로직을 담당하는, 관심사의 분리(Separation of Concerns) 를 명확히 할 수 있다.
자주 바뀌는 UI 요소에 대한 테스트 대신, 오래 유지되는 (바뀌면 치명적인) 비즈니스 로직에 대한 테스트 코드를 작성해 유지보수에 도움이 되는 테스트 코드를 치밀하게 작성할 수 있다.
Store 라는 개념을 사용해보자.
TSyringe 와 TSyringe로 구축된 Store 를 이용해서 Redux 를 만들어보자.
다음에는 관심사의 분리가 왜 좋은지 더 와닿을 것이다.