이 글은 React-Redux라이브러리를 직접 열어서 구현해본 과정을 기록 한 글입니다. 만약 정보를 얻기 위해 들어오셨다면 시간이 아까우실 테니 뒤로 가시는 게 좋습니다. 아직 내공이 부족해서 내용이 많이 부실할 예정입니다. 참고하세요.
사실 우리가 쓰던 Redux는 React-Redux였다.
Redux공식문서에 들어가면 README.md에 적혀있는 내용이다. React전용이 아닌 JavaScript앱을 위한 라이브러리이다. Core한 기능은 Redux라이브러리에 구현되어있으며, 우리가 React에서 사용하던 Redux는 Redux를 React에서 사용하기 위해 Redux를 컴포넌트로 한번 감싼 React-Redux이다.
그러므로 핵심 기능을 이해하기 위해선 Redux라이브러리를, React에서 어떻게 동작하는지 이해하기 위해선 React-Redux라이브러리를 봐야한다.
Redux 훑어보기
https://github1s.com/reduxjs/redux
GitHub1s
github1s.com
github1s 를 통해서 쉽게 여러 파일을 돌아다니며 코드를 들여다볼 수 있다.
Redux라이브러리의 src폴더를 보면 6개의 파일이 보인다.
이중에 가장 중요한 파일은 createStore.ts이다.
나머지 파일들은 middleware를 추가하는 기능, reducer를 모아주는 기능 등 Redux를 더 쉽게 사용하게 도와주는 기능들이다.
createStore를 살펴보자.
https://github1s.com/reduxjs/redux/blob/HEAD/src/createStore.ts
GitHub1s
github1s.com
음... 사실 봐도 잘 모르겠다...... 내가 만약 Redux를 사용한 적 없는 상태에서 코드를 본 게 아니라면 전혀 이해하지 못했을 것 같다. 라이브러리를 처음 열어봐서 구조도 잘 모르겠고 뭐가 중요한지 잘 모르겠다. 그리고 이걸 블로그 글로 정리할 자신은 더더욱 없다. 그러므로 내가 Redux를 구현해보면서 참조한 부분만 조금 정리해 보려고 한다.
Redux - getState()
Redux의 상태를 전부 조회할 수 있는 함수이다. currentState를 그대로 반환한다. currentState는 처음 createStore를 생성할 때 initial로 넣어준 값을 인자로 preloadState로 받는데 이 값을 내부에서 한번 더 할당해서 사용하는 걸로 보인다. createStore내의 currentState는 함수가 종료된 후에도 외부에서 참조를 하게 된다. 이러한 구조를 통해 클로저를 형성하여 가비지 컬렉팅이 되지 않는다. 변수를 은닉할 수 있다는 장점이 있어서 클로저를 이용한 것으로 보인다.
Redux - dispatch()
dispatch라고 하면 우리가 State를 변경하기 위해 사용하는 함수이다. dispatch내부에서 reducer를 이용해 currentState를 수정하고 있다. currentState를 수정하고 난 후에는 함수들로 이루어진 listeners배열을 불러와서 배열 내의 함수를 전부 실행시키는 것처럼 보인다. 솔직히 완전히 이해하지는 못했지만, 변경이 일어난 후에 listener를 동작시키는 것처럼 추측된다.
Redux -subscribe()
subscribe함수를 통해서 listener함수를 인자로 받아서, 배열에 push 하고 있다. return으로는 unsubscribe함수를 반환한다.
원하는 시점에 unsubscribe 하기 위한 목적인 것 같다.
사실 이 글을 쓰는 와중에도 1도 모르겠다.
Redux를 설명하는 가장 흔한 이미지인 것 같다. 중요한 부분은 Action을 Dispatch 하고, Store에 저장한 다음, 바뀐 부분을 UI로 반영해 준다는 점이다. 위에서 본 사진 3장이 핵심적인 과정을 수행하는 기능인 것 같다. 물론 라이브러리 내부에 더 많은 부분이 있지만, 최소한의 개념적인 부분, 내가 알아볼 수 있고 적용할 수 있는 부분을 들고 왔다.
React-Redux 훑어보기
https://github1s.com/reduxjs/react-redux/blob/HEAD/src/hooks/useSelector.ts
GitHub1s
github1s.com
React-Redux는 Redux를 React에서 사용하기 위해 한번 감싸준 라이브러리이다. 그러므로 React를 위한 components. hooks가 내장되어있다.
React-Redux로 넘어오니 익숙한 useSelector, useDispatch가 보인다. 또한 Provider, Context가 있는 걸로 보아, Redux는 Context Api도 사용하는 걸 확인할 수 있다.
React-Redux - useSelector()
우리가 React-Redux를 사용할 때 useSelector를 통해서 Store의 값을 불러온다. 함수 내부를 보니 useSyncExternalStoreWithSelector라는 이름이 긴 함수에 이것저것 넣으면 selectedState에 원하는 값이 할당되는 것 같다. 할당해주는 값 중엔 subscription, store가 있다. 이 두 가지가 중요한 것 같다. useSelector를 사용함과 동시에 subscription에 추가되고, 그 값은 store에서 가져오는 동작을 하는 것 같다.
React-Redux - subscribe
subscribe파일에 들어가게 되면, 이런 코드가 있다. notify를 통해서 State의 변경을 알리는 것 같다. 위에서 본 Redux의 listeners와 비슷하다. 다만 Redux랑은 다르게 Linked List를 사용했다. 모든 listener를 동작시키는 로직이기 때문에, 구체적인 Index를 참조할 필요가 없어서 Linked List로 처리한 것 같다.
여기서 한번 정리.
React-Redux와 같은 상태 관리 라이브러리를 동작시키기 위한 최소한의 코드를 살펴봤다. 생략한 부분이 많기는 하지만, 이 정도만 보고도 구현 정도는 할 수 있다. 라이브러리를 열어보기 전에는 의문이었던 부분이 있었는데 대부분 해결이 된 것 같다. 내가 의문이었던 부분은 아래와 같다.
- 상태는 어떻게 저장하지?
- 클로저를 활용해서 객체를 은닉화 한다.
- 상태가 바뀐걸 어떻게 모니터링하지?
- 배열 및 Linked List에 callback함수를 담아서 dispatch이후에 반복해서 전부 실행시키면 된다.
- Provider는 무슨 역할을 하지? 왜 필요한 거지?
- createStore에서 반환한 함수들을 전역으로 사용하기 위함이다.(getStore, subscribe, dipatch 등등)
이 정도만 이해해도 React-Redux 비슷한 무언가는 만들 수 있을 것 같다.
이제부터 구현 시작.
Redux를 내가 구현했기 때문에, 내 이름을 따서 hedux라고 이름을 정했다.
hedux를 구현한 폴더 구조이다.
core : createHeduxStore함수 작성.
hooks : hedux를 사용하기 위한 useDispatch, useSelector작성
modules : 기능별로 작성된 파일. auth 관련된 데이터를 담고 있는 auth가 작성돼있다.
reducer : 값을 state에 반영하기 위한 순수 함수.
Redux는 combineReducer 같은 함수를 지원하지만, 나는 그것까지 구현하지는 않았다. 그러므로 reducer에 전부 모아서 작성을 했다. 물론 action type까지도 디테일하게 구현하지는 않았다. 왜냐하면 더 이상 확장할 생각이 없기 때문이다.......
hedux/core/hedux - CreateHeduxStore.(Github)
import { Hedux, HeduxReducer } from './types';
function createHeduxStore<T>(initState: T, { reducer }: HeduxReducer<T>): Hedux<T> {
let state: T = initState;
const observer: any[] = [];
const getState = (key: keyof T): Partial<T> => {
const data = state[key];
return data;
};
const dispatch = (type: string, payload?: { [k: string]: any }) => {
state = reducer(state, { type, payload });
reflect();
};
const reflect = () => {
observer.forEach((rerender) => rerender());
};
const subscribe = (trigger: () => void) => {
observer.push(trigger);
};
return { getState, dispatch, reflect, subscribe };
}
export default createHeduxStore;
위에서 내가 참고한 코드들을 토대로 작성했다. 간단히 요약해 보겠다.
우선 state를 저장하기 위한 state변수, 변화를 감지하기 위한 observer배열이 최상단에 선언되어있다.
그다음은 getState, dispatch가 있다. 이 함수를 통해서 상태를 불러오고, 수정할 수 있다. dipatch함수는 store에 데이터를 수정한 후, reflect라는 함수를 동작시킨다. reflect함수는 observer내부의 모든 trigger함수를 동작시킨다.
마지막으로 reflect와 subscribe가 있다. 이 두 함수를 통해서 변화를 감지할 함수들을 설정하고, 반영시킬 수 있다.
이 모든 함수들을 return 해 줌으로써 createHeduxStore함수는 마무리가 된다.
hedux/reducer/reducer - reducer.(Github)
import { HeduxActionObj } from './types';
function reducer<T>(state: T, action?: HeduxActionObj): T {
if (!action) {
return {
...state,
};
}
if (action.type === 'auth') {
return {
...state,
auth: {
...action.payload,
},
};
}
return {
...state,
};
}
export default reducer;
combineReducer함수를 구현하지 않았기 때문에, 일단 이곳에서 reducer를 전부 선언하기로 했다. 덤으로 actionType도 따로 저장하지 않았다. 나중에 확장해서 사용할 때 분리하려고 한다.
보이는 그대로 action.type이 'auth'일 때만 state에 반영하고, 그게 아닐 경우 state그대로 반환하게 된다.
hedux/modules/auth - auth.(Github)
import { authTokenKey, persistStore } from '../../persistStore/persistStore';
export const heduxAuthInitState = {
auth: { isLoggedIn: !!persistStore.get(authTokenKey) },
};
export type HeduxAuthType = typeof heduxAuthInitState.auth;
이곳에서는 auth기능을 담당할 객체를 선언해주었다. 초기값을 설정하고 export 시켜준다. export시켜준 변수는 바깥의 index.ts에서 모아준다. 그리고 나중에 useSelector에서 사용할 type도 하나 만들어 줬다. Generic Type에 익숙하지 않아서 어쩔 수 없이 새로 선언해서 export 시켜줬다.
index.ts - Store 및 Context생성.(Github)
const heduxStore = createHeduxStore<HeduxInitState>(heduxInitState, { reducer });
export const HeduxStore = createContext(heduxStore);
root.render(
<React.StrictMode>
<HeduxStore.Provider value={heduxStore}>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</HeduxStore.Provider>
</React.StrictMode>,
);
초기값과 reducer를 인자로 하여 heduxStore를 드디어 생성했다. 그리고 Context Api를 위한 Context도 생성해줬다. App컴포넌트를 HeduxStore.Provider로 감싸줌으로써 내부에서 dispatch나 subscribe기능을 사용할 수 있게 되었다. 이제 준비는 모두 끝이 났다. Provider내부에서는 heduxStore에서 반환한 함수들을 언제든지 가져다 쓸 수 있고, 은닉화된 State를 참조할 수 있게 되었다.
Custom Hooks 만들기.
useDispatch 작성.(Github)
import { useContext } from 'react';
import { HeduxStore } from '../..';
function useDispatch() {
const { dispatch } = useContext(HeduxStore);
return (type: string, payload: object) => {
dispatch(type, payload);
};
}
export default useDispatch;
우리의 Action을 Dispatch 해줄 useDispatch함수다. Context Api로 받아온 hedusStore함수 중에 dispatch를 구조 분해 할당해주고, type과 payload를 dispatch 하면 state에 반영된다.
useSelector 작성.(Github)
import { useContext, useEffect, useState } from 'react';
import { HeduxStore } from '../..';
import { HeduxInitState } from '../moudles';
function useSelector<T>(key: keyof HeduxInitState): T {
const storeState = useContext(HeduxStore).getState(key);
const { subscribe } = useContext(HeduxStore);
const [state, setState] = useState(Math.random());
useEffect(() => {
subscribe(() => setState(Math.random()));
}, []);
return storeState;
}
export default useSelector;
store에서 원하는 데이터를 받아올 useSelector함수다. state객체에서 원하는 key값을 참조할 수 있게 작성했다. 우선은 state의 키에 해당하는 객체를 전부 반환해주기로 했다. useSelector처럼 콜백 함수 형태로는 구현하지 못했다.
코드를 보면 subscribe함수를 사용한다. useEffect에서 컴포넌트가 마운트 되는 시점에 subscribe에 변화를 반영할 trigger를 추가해 주었다.
React는 setState동작을 통해 state가 변경되면 리 렌더링을 한다. 이 이외의 방법으로는 어떻게 리렌더링을 시키는지 잘 모르겠다. 그래서 나는 setState를 랜덤 하게 바꾸는 함수를 trigger로 만들어서 subscribe 하게 만들었다. 이렇게 되면 store에 새로운 값이 dispatch 될 때마다 reflect함수를 통해서 등록한 모든 함수가 동작하게 되면서 useSelect도 리 렌더링이 되기 때문에 새로운 값을 불러올 것이다.
이제 사용해보자.
src/App.tsx
function App() {
const { isLoggedIn } = useSelector<HeduxAuthType>('auth');
return (
<Container>
<Router>
<Routes>
{isLoggedIn ? (
<>
<Route path={routes.home} element={<HomeScreen />} />
<Route path={routes.todo} element={<HomeScreen />} />
</>
) : (
<>
<Route path={routes.home} element={<LoginScreen />} />
<Route path={routes.join} element={<SignUpScreen />} />
</>
)}
</Routes>
</Router>
</Container>
);
}
useSelector를 통해 store내부의 auth객체를 전부 들고 오고, 그중에서 isLoggedIn객체를 가져온다. 위쪽의 module의 auth파일을 보면 초기값은 localStorage에 authToken이 있는지 없는지 여부이다. 만약 토큰이 있다면 isLoggedIn객체의 value는 true가 되어, home화면으로 들어오게 된다. 그렇지 않으면, 로그인 화면이 보이게 된다.
src/screen/login/LoginFormTemplate.tsx
const dispatch = useDispatch();
const loginRequest = ({ email, password }: LoginParams) => {
const onSuccess = ({ token }: LoginResponse) => {
window.alert('로그인이 완료되었습니다.');
persistStore.set(authTokenKey, token);
dispatch('auth', { isLoggedIn: true });
};
const onError = ({ response }: LoginError) => {
return response?.data && window.alert(response?.data.details);
};
mutate({ email, password }, { onSuccess, onError });
};
로그인을 할 때, 이전까지는 token을 localstorage에 저장한 다음, 새 로고 침하여 값을 읽어오는 방식을 적용했다. 하지만 이제 store에서 관리할 수 있게 되었다. onSucess함수 내에서 dispatch를 함으로써 isLoggedIn객체를 수정한다.
createStore내의 dispatch함수는 state를 수정한 다음에 reflect함수를 통해 observer내부의 모든 trigger함수를 동작시킨다. useSelector함수 내에서 setState의 값을 랜덤 하게 변경하는 함수를 등록시켰으니, useSelector는 리 렌더링 될 것이고, isLoggedIn은 수정된 값을 반영하여 View를 변화시키게 된다.
구현 내용.
로그인에 성공할 경우 dispatch함수에 의해 state가 변경되고, subscribe 하고 있는 hook들의 리 렌더링을 발생시킵니다. 리 렌더링 될 때 store에서 변경된 isLoggedIn:true의 값을 불러와 Home화면의 컴포넌트들을 렌더링 하게 됩니다.
최소한의 기능은 구현 완료.
작성한 코드만 놓고 보면 dispatch, store, view의 기능들이 동작하기는 한다. 로그인에 성공하면 isLoggedIn값을 변경하고, 변경을 감지하고 view를 변화시키기 때문이다. 코드를 정리하지 못해서 가독성이 떨어지긴 하지만 이 정도로 우선은 만족해보려고 한다. 아직 구현 못한 기능들은 아래와 같다.
- combineReducer
- 이 함수가 있어야 duck구조로 깔끔하게 module을 관리할 수 있을 것 같다.
- unSubscribe
- 현재는 subscribe밖에 구현이 안되어있다. 이대로 두면 메모리 누수가 발생하는데, App컴포넌트 내에서만 useSelector를 사용하기 때문에 unmount, mount동작이 없을 것으로 예상되어 지금 상태에서는 문제가 발생하지 않을 것 같다.
- Context Api 잘 숨기기
- 현재 index.ts에서 context를 생성하고 export 시키고 있다. React-Redux에서는 해당 부분이 노출이 안되어있는데, 이 부분을 더 깔끔하게 정리할 수 있는 방법을 찾고 싶다.
물론 기타 다른 기능들도 많지만, 내가 직접 사용했던 Redux의 기능 중에는 이 세 가지를 아직 구현하지 못했다. 나중에 내공이 더 쌓이면 구현하도록 해봐야겠다.
마치며.
사실 나는 라이브러리를 열어서 코드를 본적이 한 번도 없다. 라이브러리의 코드는 복잡하고 어려워서 내가 이해하지 못할 거라고 생각했다. 어쩌면 그냥 이해할 생각을 안 하고 있었던 것 같다. 하지만 실제로 코드를 열어보면 전혀 반대다. 고수들이 작성한 코드는 오히려 간단하고 이해하기가 쉽다. 코드의 복잡도를 낮추기 위해 기능별로 파일을 분류하고, 적절한 단위로 분리되어있기 때문이다. 복잡하고 이해하기 힘든 코드는 내가 짠 코드에 더 어울리는 말 같다. 또한 코드의 절반 정도는 에러 핸들링, 주석 관련된 내용이다. 코드량이 많아 보이지만 실제로 뜯어보면 핵심 로직은 몇 줄 안 된다는 의미이다.
이번 경험을 통해서 라이브러리에 대한 환상과 두려움이 많이 사라진 것 같다. 하나하나 뜯어보면 그렇게 어려운 기술이 사용된 것도 아니었다.
공식문서와 코드를 직접 살펴봄으로써 subscirbe에 Linked List를 사용하는 것을 새롭게 알게 되었다. 다른 블로그에서는 관련 내용을 다루지 않았던 것 같은데, 직접 코드를 보니 얻을 수 있는 정보였다.
아직은 라이브러리 내부를 보는 게 익숙하지는 않지만, 내공을 쌓다 보면 이해하는 속도가 점점 빨라질 것이라고 기대한다.
부족한 부분의 개선은 2편에서 이어집니다.
React-Redux 간단하게 따라 만들어보기 - 2편.
React-Redux 간단하게 따라 만들어보기 - 1편. React-Redux 간단하게 따라 만들어보기. 이 글은 React-Redux라이브러리를 직접 열어서 구현해본 과정을 기록 한 글입니다. 만약 정보를 얻기 위해 들어오셨
2hakjoon-mindmap.tistory.com
'라이브러리 탐방' 카테고리의 다른 글
React-Redux 간단하게 따라 만들어보기 - 2편. (4) | 2022.09.01 |
---|