Web: 상태 관리 라이브러리란 (개념, Redux 예시)

Heechan
HcleeDev
Published in
11 min readMar 11, 2022

--

Photo by NASA on Unsplash

어지간히 변화가 없는 웹 페이지를 만드는 것이 아니면, 웹 애플리케이션에서 변하는 요소를 관리하는 것은 필수적이다. iOS를 할 때도 그랬고, React를 사용할 때도 이를 위한 State라는 녀석들 계속 사용하게 된다.

React에서는 이런 상태들을 조금 더 좋게 관리하기 위한 다양한 라이브러리가 있다. 우리 회사에서는 MobX를 사용하고 있다. 사용해보니까 편하긴 한데, 왠지 전역으로 상태를 관리한다는게 어째서 좋은건지, 필수적으로 다들 사용하고 있는건지 궁금해졌다.

이번 주는 이 상태 관리 라이브러리는 무엇인지, 왜 사용해야 하는지 알아보자.

상태 관리란

웹 개발을 하다보면 정말 다양하고 세세한 부분에서 State를 관리해야 한다.

예를 들어 무언가를 눌렀을 때 화면에 보이는 숫자가 하나 올라가게 만든다고 생각해보자. 이때의 처리 흐름을 생각해보자.

어떤 공간에 저장되어있는 숫자 변수 num에 1을 더 해야 할 것이다. 그러고 나서 이 변수가 변경되었다는 사실을 관련된 컴포넌트에 알려야 할 것이다. 여기서 num 을 참조하고 있는 컴포넌트는 값이 변경되었다는 소식을 듣고, 리랜더링하게 된다. 리랜더링된 컴포넌트는 이제 변화된 숫자를 화면에 보여준다.

이거를 React에서 일반적으로 구현하는 방법은 아래와 같을 것이다.

위 코드를 생각해보면, 숫자가 들어있는 div 박스를 클릭하면 setNum 메서드가 호출된다. 그러면 num 값이 변경된다는 것을 알 수 있고, 관련된 컴포넌트를 리랜더링하게 될 것이다. 리랜더링이 일어난 후에는 새롭게 더해진 num 의 값이 화면에 보이게 될 것이다.

근데 지금은 익숙해졌지만 처음에 이걸 알았을 때는 바로 num 에 어떤 값을 할당할 수 없고 setter를 따로 호출해서 값을 변경해줘야 한다는 것이 약간 불만이었다. 하지만 이렇게 하는 이유는 이런 주어진 setter를 활용해야 관련 컴포넌트들이 제대로 리랜더링될 수 있도록 알림이 가게 할 수 있기 때문이다.

이 방법으로 개발을 할 때 가장 귀찮았던 것은 바로 이 상태를 하위 컴포넌트로 넘겨줘야 하는 경우가 많아질 때다.

만약에 위처럼 간단한 코드가 아니라 어쩌다 한 계층 더 내려갔다고 생각해보자.

뭐 어떻게 되어먹은 페이지인지 알아보기 보다는, 구조 정도만 생각해보자. 모종의 이유로 하위 컴포넌트인 Number 에서도 num, setNum 이 필요해졌고, 그걸 위 코드처럼 넘겨준 상황이다.

이게 지금 한 계층차, 한 컴포넌트에 대해서만 넘겨주고 있으니 크게 불편함이 보이지 않는데, 만약 하위 컴포넌트들의 깊이가 훨씬 깊어지거나 받아야 하는 수가 많아지면 저렇게 넘기는 것조차 고역이다. A가 하위 컴포넌트 B 속에 있는 C 컴포넌트에게 상태 정보를 넘기고 싶다면, B는 이걸 직접 사용하지 않더라도 단지 넘겨주기 위해서 이를 가지고 있어야 한다.

그리고 여기저기 넘겨주다보면 어디서 어떤 연유로 상태가 업데이트되고 화면이 리랜더링되는건지 알아채기 힘들 수 있다. 적어도 업데이트하는 함수가 한 곳에라도 있으면 어디서 어떻게 부르는지 쉽게 확인이라도 할 수 있을텐데, 이런 경우에는 의도치 않게 매번 모두 Command + F를 눌러가며 여러 파일들을 휘젓게 될 것이다.

상태 관리 라이브러리를 왜 사용해야 할까

그런 불편함을 해결하기 위해 다양한 상태 관리 라이브러리들이 존재한다. 가장 대표적인 것은 Redux가 있고, 우리 회사에서 사용하는 MobX도 있다. 오늘 검색해보니 Recoil이라는 신흥 라이브러리도 있는 것 같다.

위처럼 useState 를 사용했을 때와 달리, 이런 라이브러리들은 대부분 전역 상태 관리를 지원한다. useState 가 위처럼 한 컴포넌트에서 만들어지고 그 하위의 상태를 넘겨받은 컴포넌트들만 가지게 되지만, 전역 상태 관리 라이브러리를 사용하면 꼭 그렇게 인접한 컴포넌트끼리 넘겨주는 방식을 거치지 않더라도 여기저기 컴포넌트에서 접근할 수 있다. 일단 Props가 무지막지하게 복잡해지는 문제는 해결될 것만 같다.

그런데 생각해보면, 지금껏 개발을 해오면서 전역 변수는 가급적이면 사용하지 말라는 말만 들었지 사용하라는 말은 못들어봤다. 그럼에도 웹 프로젝트에서는 의외로 이런 전역 상태 관리 라이브러리를 필수적으로 사용하는 경우가 많은 것 같다.

전역 변수를 사용했을 때 가장 안좋은 점은 어디서 접근해서 값을 변경했는지 모르고, 특정 경우에서는 스레드 세이프 여부도 생각해야 한다는 문제가 있다. 하지만 이 상태 관리 라이브러리들은 전역이지만 오히려 이런 걱정을 덜게 해주는 구조를 가지고 있다.

이런 라이브러리의 경우 진짜로 값을 할당하는 부분은 한 곳에서만 일어난다는 점이 그 특징이다. 아래 코드는 무슨 라이브러리는 아니고, 그냥 느낌만 설명하기 위해 간단히 적어놓은 코드다.

let a = { num: 1 }...a.num = 2;=====================let a = { num: 1 }function setNum(value) {
a.num = value; // num의 업데이트는 오직 여기서만 일어나도록
}
...setNum(2);

상단 코드에서는 그냥 num 에 직접 접근해 값을 업데이트해주고 있는데, 이런 식으로 만들면 어디서 어떻게 접근해서 막 바꾸는지 알기 힘들다. 막 하위 컴포넌트로 넘겨주고 하다보면 더 지옥이 될 것이다.

하단 코드는 이제 num 의 실질적인 업데이트는 setNum 메서드 안에서만 일어나고, 다른 곳에서는 num 을 업데이트할 때는 setNum 만 사용하도록 할 수 있다. 이렇게 하면 보다 명확히 어디서 setNum 을 사용하는지 확인할 수 있을 것이다. 이 setNum 이 전역에서 접근할 수 있도록 만들어지긴 하지만, 요즘 IDE는 이 메서드가 어디서 사용되는지 정도야 쉽게 파악할 수 있다. 그리고 상태를 업데이트하는 로직을 분리해서 관리할 수 있다는 장점이 있다.

결국 이런 라이브러리의 공통적인 특징은 상태 업데이트부터 컴포넌트 리랜더링까지 데이터의 흐름을 한 방향으로, 한 지점을 거쳐가도록 설정하도록 함으로써 장점을 챙기고 있다는 점이다.

이를 간단하게 그림으로 그려보면 다음처럼 표현할 수 있을 것 같다.

이제 인접한 컴포넌트끼리 넘겨줘야 하는 것이 아니라 전역 공간의 Store에 들어있는 상태에 접근해 값을 가져올 수 있다.

또한 Store 안에 있는 상태의 값을 변경하기 위해선, 바로 접근하는 것이 아니라 Store에서 제공하는 메서드를 활용해야 한다. 그 과정이 빨간색 화살표라고 볼 수 있다.

그러고 상태가 변경되었을 때 이 상태를 참조하고 있는 컴포넌트에 리랜더링하라는 알림을 보낸다. 그 과정을 주황색 화살표로 표현한 것이라고 볼 수 있다.

상태 관리 라이브러리를 이용하면 위와 유사한 느낌의 구조를 형성할 수 있다.

간단한 예시 — Redux

그래서 가장 대표적인 전역 상태 관리 라이브러리인 Redux의 구조를 알아보고 간단히 코드로 알아보도록 하자.

한 번 간단하게 Redux를 이용했을 때 생길만한 구조를 이미지로 만들어보았다.

일단 기억해야 하는 존재는 컴포넌트, 더해주라는 Action, Reducer, 그리고 Store 정도다. 위 그림의 흐름을 정리해보자.

  • 컴포넌트에서 전역 Store에 관리되고 있는 상태 num 에 1을 더하고자 한다.
  • 컴포넌트에서 상태에 직접 접근할 수 없고, Store에서 제공하는 방법을 사용해야 한다. 여기서 Reducer를 이용한다. Reducer는 어떤 Action을 원하는지 확인하고 그에 따라 상태를 업데이트해주는 함수다.
  • ReducerAction으로 { type: ‘ADD’ } 를 보내주면 num 에 1을 더해주는걸로 미리 약속을 해둔 상태다.
  • 컴포넌트ReducerAction { type: ‘ADD’ }를 발송(dispatch)한다.
  • Reducer는 이 요청을 받고 내부에 있는 상태, num 에다가 1을 더해준다. 즉, 상태를 업데이트한다.
  • 상태가 업데이트된 후 이를 참조하고 있는 컴포넌트들에게 num 이 업데이트되었으니 리랜더링을 해야 한다고 알린다.

이 구조가 저기 위에서 얘기했던 데이터의 흐름을 한 방향으로, 한 지점을 지나도록 하는 전역 상태 관리 라이브러리의 구조라고 생각할 수 있다.

일단 데이터 변경부터 리랜더링까지 한 방향으로 진행이 되고 있다. 또한, 상태에 직접 접근해 변경하는 것은 불가능하므로, 반드시 Store의 Reducer를 거쳐야 한다. 공통적으로 한 지점을 지나가도록 만들어졌고, 업데이트 로직도 분리되어있음을 알 수 있다.

코드로도 한 번 훑어보자. 처음으로 Reducer를 구성해보자.

const initialState = {
num: 0
}
export const numReducer = (state = initialState, action) {
switch (action.type) {
case 'ADD':
return {
...state,
num: state.num + 1,
}
default:
return state
}
}

위에서 말한 것처럼 Reducer는 Action이 무엇을 원하는지 확인하고, 그에 따라 적당한 상태 업데이트를 진행한다. 여기서는 'ADD' 를 받게 되면 상태들 중 num 에 1을 더해주도록 설정했다.

그러면 이 Action을 만들어내는 코드도 봐보자.

export const add = () => ({
type: 'ADD'
})

이렇게 만들면 컴포넌트에서 add() 를 사용하면 { type: 'ADD' } 라는 Action을 얻어서 사용할 수 있을 것이다.

그러면 이제 전역에서 접근할 수 있는 Store를 만들어서 상태와 Reducer를 담아보자. 이번에는 가장 루트 컴포넌트가 있는 곳으로 한번 와보겠다.

const store = createStore(numReducer);

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

여기서 createStore 메서드를 이용해 아까 만들었던 numReducer 를 가져와 Store를 만들어주었다. 이제 이 store 에다가는 numReducer 와 약속한 Action을 주고 받으면서 상태을 업데이트할 수 있을 것이다.

위 코드에서 또 주목해야 하는 곳은 <Provider store={store} />다. 이 Provider는 내부 컴포넌트에서 이 Store에 접근할 수 있게 해준다. 지금 이 경우는 전체 컴포넌트의 루트인 <App /> 를 감싸줬기 때문에 진짜 애플리케이션 전역에서 접근할 수 있다고 생각할 수 있다.

그러면 실제로 상태를 사용할 App.js 내부를 확인해보자.

const App = () => {
const { num } = useSelector(state => state.numReducer);
const dispatch = useDispatch();
const onAdd = () => dispatch(add());
return (
<div onClick={onAdd}>{num}</div>
)
}

useSelector 를 이용하면 numReducer 가 가지고 있는 값에 접근할 수 있다. 여기서 num 을 꺼내와 아래 화면에서 사용하고 있다.

useDispatch 를 통해 App을 감싸고 있는 Provider의 Store의 Reducer에 Action을 보낼 수단을 받을 수 있다. dispatch 에 발송이라는 뜻이 있는 것처럼, 우리가 원하는 Action을 Reducer에 보낼 수 있을 것이다.

밑에 있는 div 를 클릭할 때마다 onAdd 가 발동이 될텐데, onAdddispatch 를 이용해 우리가 아까 만들었던 add() , 즉 { type: 'ADD' } 라는 Action을 보낼 것이다.

Redux를 이용하면 이렇게 전역 Store에서 상태를 관리, 업데이트하도록 만들 수 있다.

결론

사실 꼭 이걸 사용해야 훌륭하고 좋은 앱을 만들 수 있는 것은 아니다. 충분히 기본적인 것으로도 만들 수 있을 것이다. 지금은 간단히 보았지만 많이 사용하다보면 성능이라든가 수많은 상태들을 어떻게 관리할 것인가에 대해서도 고민하게 될 것이다.

이런 라이브러리를 적재적소에 잘 사용한다면 개발 과정과 디버깅 과정이 편해지고 코드 구조가 보다 깔끔해질 수 있기에 많은 사랑을 받는 것 같다.

이왕 이렇게 된거 다음주는 MobX의 개념과 간단한 사용법에 대해 얘기해봐야겠다.

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science