React 의 훅 그리고 Context API 에 대한 추상적인 고찰

반응형

React 의 훅 그리고 Context API 에 대한 추상적인 고찰.

 

React 훅은 마치 javascript 의 클로저 처럼 함수가 하나의 실행 영역을 가지는 것과 유사하게 파라미터를 받을 수 있고(마치 리액트 컴포넌트의 속성과 같이) 내부에서 사용될 수 있는 변수들이 존재하며 상태(state)와 동작(method) 을 가지는 기능 단위이다. 보통 하나의 기능 단위는 상태와 동작으로 정의될 수 있다. 그리고 함수형 프로그래밍 방식에서 추구하는 확실한 기능 단위의 조합 방식에도 부합한다. 리액트는 UI 라이브러리이며 그 기본 단위인 컴포넌트는 생명주기, 렌더링 관련된 기능과 속성, 상태, 동작과 같은 것을 가진다. 훅이 나오기 전 클래스 형 컴포넌트를 주로 사용하던 시기에 단순한 독립적인 유틸리티 함수들이 아니라 컴포넌트의 생명주기나 렌더링 기능 등의 시점이나 흐름의 변화를 주려고 하면 그 또한 컴포넌트를 사용할 수 밖에 없었다. 앱의 상태값이나 네트워크 접속 상태 또는 웹소켓의 기능과 같이 UI 와 접목은 필요하지만 UI 자체는 필요없는 이러한 부분이 꽤나 많이 존재하였다. 또한 성격상 이러한 것들이 클래스형 컴포넌트의 생명주기 함수들과 함께 조건에 따른 렌더링을 하는 등의 코드를 작성하다보면 뭔가 코드가 지저분해지고 차후에는 문제와 마주치게 된다. 이는 불변성, 순수함수 등등 함수형 개발방식의 패러다임에 맞지 않는 명령형 로직이 잘 맞지 않는 부분에 그러한 방식으로 처리하려고 했기 때문이며 또한 그것에 대한 특별한 공식적인 기능은 없었다. 물론 실력있는 개발자의 경우 이러한 부분도 자체적으로 훅과 같은 것을 만들어 사용하였을 것이다. 이러한 것을 위한 것이 훅이며 이로서 리액트를 완전히 함수형 컴포넌트로만 작성이 가능하도록 변화를 가져오게 해주었다.

 

Context API 는 Provider 로 감싸고 그 아래 손자의 손자에 해당하는 자식 컴포넌트들이 해당 기능을 속성값의 직접적인 전달 없이 사용할 수 있도록 리액트에서 제공하는 기능이다. 아주 단순하게 createContext 하고 Provider 로 감싼 다음 useContext 를 사용하여 사용하는 방법이 있고 또한 더 나아가 useReducer 를 사용하여 Dispatch<Action> 형식으로 하나의 컨텍스트로 여러 상태값을 동시에 제어할 수 있는 방식도 있다. 여기에 해당 Context 를 useContext 를 통해 사용하는 것이 아니라 해당 기능에 대해서 모아서 손쉽고 실수없이 사용할 수 있도록 커스텀 훅을 만들어 제공하기도 한다.

 

여기서 조금 혼동이 될 수 있는 개념이 Context API 는 자식들에서 컨텍스트의 상태와 동작을 사용하려면 Provider 로 감싸주어야 하는데 이것이 마치 훅 또한 그렇게 Provider 와 같은 것으로 감싸져 있어야 하는 것 아닌가 하는 오해가 생길 수도 있다. Context API 와 훅은 다른 개념으로 Context API 는 몇계단 아래에 있는 자식 컴포넌트들이 한참 위의 부모의 상태와 기능을 모아놓은 컨텍스트를 사용할 수 있게 하기 위한 리액트가 제공하는 기능이며 훅은 마치 함수의 클로저와 같이 함수 단위의 상태와 동작을 가지는 기능 단위 로직을 리액트 컴포넌트들과 적절히 사용될 수 있도록 제공하는 기능이다.

 

redux 는 전역 state 용 라이브러리이고 (redux 가 ContextAPI 로 구현된 건 아니고 react-redux 가 ContextAPI 로 구현됨. react 에 redux 를 가져와서 사용하기 위한 라이브러리) 이에 반해 useSWR 은 단순히 캐싱 기능을 잘 구현한 웹요청 라이브러리이며 Context API 를 사용하지 않았지만 useSWR 이라는 훅을 통해서 각 컴포넌트 들에서 마치 전역 공간에 상태값이 존재하는 듯이 사용할 수 있다. SWR 의 경우 axios 같은 웹요청 라이브러리로 캐싱 기능을 잘 구현한 것이고 그러한 것이 함수 단위로 또한 다른 단일 자원 공간에 위치한 것을 이용하기 때문에 마치 Context API 의 Provider 와 유사하게 보여질 수 있어 어떻게 Context API 도 사용하지 않았는데 이런게 가능할까라는 약간의 혼동이 올 수도 있다. 

 

단순하게 말하면 훅을 통해서 단일 전역 자원의 기능을 제공할 경우 이것은 마치 Context API 처럼 최상단에 위치한 컨텍스트와 유사하게 해당 단일 전역 자원에 대한 기능을 제공할 수 있는 것이다. 또한 Context API 의 구현에 있어 커스텀 훅을 사용하면 또한 이러한 부분을 편리하게 제공해줄 수 있는 것도 이와 같은 이치이다.

 

따라서 WebSocket 관련 리액트 쪽 로직을 구성할 경우 useWebSocket 과 같은 커스텀 훅을 만들어서 사용하는 것이 가장 추천할 만한 방법 같다. 과거에 ViewWebSocket 이라는 컴포넌트를 만들어서 사용한 적이 있었는데 왜 이렇게 사용할 수 밖에 없었는가 한번 반성을 하면서 고찰을 남겨본다. => useWebSocket 을 사용하는 것은 좋지 않다. 그냥 단순히 유틸리티 함수로 만들어 사용하는 것이 좋다.

 

redux 는 Context API 를 사용하기 때문에 react-redux 로 부터 import 한 Provider 의 store 에 셋팅한 store 를 넘겨주어 사용한다. 그래서 useSelector 같은 것을 사용하려면 해당 Provider 의 자식 컴포넌트에서만 사용이 가능하다. react-redux 로 부터 import 하여 사용하는 useDispatch 또한 Provider 하위의 컴포넌트이어야 가능하다. 하지만 store 에서 직접 useAppDispatch 와 같이 하여 export 한 것을 사용할 수도 있다.

 

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export const useAppDispatch = () => useDispatch<AppDispatch>();

 

여기서 useDispatch 도 react-redux 로 부터 import 한 것이지만 store.ts 자체가 store 셋팅하는 모듈이며 직접 이 store 로 부터 이러한 기능들을 import 하여 사용하면 Provider 의 하위가 아니더라도 store 로 부터 가져올 수 있기 때문에 useDispatch 같은 경우는 Provider 의 자식이 아니더라도 사용할 수가 있다.

useSelector 의 경우는 Provider 의 자식이어야만 사용할 수 가 있다. 중요한 차이점은 Context API 의 의미로 부터 찾아볼 수가 있는데 Context API 는 상태값을 그리고 그 상태값을 변경할 수 있는 기능도 제공하는 동작을 속성값의 단계적인 전달이 아니라 직접적으로 바로 사용할 수 있게 하기 위한 것인데 여기서 중요한 것은 상태값이라는 것이고 React 에서 이러한 상태값은 UI 의 렌더링과 직접 유기적으로 관찰되어지는 특수한 값이라는 것이다. useRef 를 사용한다던가 그냥 지역변수를 사용하는 것과 달리 useState 를 통해서 얻는 상태값 그리고 속성값 같이 관찰되어지면서 UI 의 리렌더링을 일으키는 그런 변수값이다. useSelector 를 통해서 리턴되어지는 값 또한 그러한 상태값이 된다. 따라서 그러한 관찰되어지는 UI 의 리렌더링을 일으키는 상태값으로 사용되어지려면 Provider 아래에 존재해야한다. 즉, Context API 의 기능을 사용하는 방식이어야 한다.

useDispatch 와 같은 경우에는 store 라는 것에 직접적으로 dispatch 만 하는 기능을 하는 것을 리턴 받는 것이기에 Provider 의 아래 단계의 컴포넌트가 아니더라도 직접 store.ts 로 부터 import 하여 사용할 수가 있다.

 

WebSocket 리액트 쪽 기능 구현을 위해서 useWebSocket 이라는 커스텀 훅만을 만들어 사용할 경우 이렇게 UI 의 렌더링과 유기적인 연관 기능 갖는 기능을 구현하기가 어렵다. useWebSocket 이라는 것을 통해서 리턴 받는 것은 단순히 연결된 socket 객체와 연결을 끊을 수 있는 함수 정도일 것인데 해당 훅 내부에 new WebSocket('ws://blurblur.com'), ws.onopen, ws.onmessage, ws.onerror, ws.onclose 를 구현할 경우 useSelector 와 같은 것을 사용할 수가 없어 모든 기능을 전천후로 다 가져와서 사용할 수 있는 여건이 되지 못한다. dispatch 는 store 로 부터 직접 가져와 사용할 수 있겠지만 전역 상태인 리덕스로 부터 상태를 읽어오는 기능은 사용이 힘들 것이다. 커스텀 훅 내에서 useState, useEffect 같은 것을 사용할 수 있으므로 상태값 같은 것을 만들어내지 못하는 것은 아니다. 이러한 경우 useSelector 까지도 사용할 수 있게 리액트의 컴포넌트의 트리 구조 상에서 위치하여 사용까지 가능하게 하려면 Provider 아래의 컴포넌트로서 EventEmitter 를 사용하여 해당 컴포넌트에서 이벤트 리스너를 등록하고 해제하는 것을 useEffect 를 통해서 넣어두고 해당 EventEmitter 의 emit 을 useWebSocket 커스텀 훅의 onopen, onmessage, onerror, onclose 에서 서버로부터 이벤트를 받을 시 발생하도록 하면 될 것이다. 일단은 UI 와 유기적으로 그리고 useSelector 를 사용하여야 할 기능이 없어보이므로 단순히 useWebSocket 구현만을 통해 서버로 부터 이벤트를 받으면 그에 대한 처리를 해주고 필요하면 dispatch 도 해주며 그리고 서버로 메시지를 보내는 정도면 될 것 같다. 그래서 useWebSocket 을 통해서 리턴받을 수 있는 것은 [socket, disconnect, wsSend] 정도가 될 것 같다. wsSend 는 임의로 붙여본 이름인데 서버로 메시지를 보내는 함수이다. 또한 이렇게 리액트 클라이언트와 서버와 통신에 있어서도 메시지의 형식은 utf8 stringify 된 json 형태의 그리고 reducer 의 액션과 같은 형태이면 좋을 것 같다.

 

하지만... 구현하려다 보니 당장에 UI 와의 연관된 로직이 필요함을 알게 되었다. 채팅 메시지를 받았을 경우 웹소켓의 역할은 앱이 active 상태일 경우에 그에 대한 이벤트를 받아서 안 읽은 메시지 숫자를 표시한다던가 하는 기능인데 이러한 기능에 벌써 UI 와의 결합이 필요하고 useSelector 의 사용이 필요해보인다. 이렇게 새로운 채팅 메시지 수신의 이벤트를 서버로 부터 수신하는 것에 대한 구체적인 처리는 useWebSocket의 onmessage 상에서 하기보다 UI 쪽의 구현 로직 쪽에 상태값과 함께 들어가는 것이 맞아보이며 useWebSocket 쪽에는 좀 더 범용적으로 EventEmitter 를 emit 하는 정도로 구현하는 것이 맞는 것 같다. 따라서 해당 EventEmitter 를 UI 쪽에서 import 하여 리스너를 등록할 수 있도록 제공해주면 될 것으로 보인다. useWebSocket 쪽에 이러한 것을 세세히 상태값을 만들어 제공해주는 방식으로 구현은 힘들어 보인다. 리스너는 각자 필요한 컴포넌트에서 등록 해제 해주면 될 것이다. 가령 채팅방이라던가 대화목록 등.

 

https://medium.com/grapecity/react-hooks-how-to-transition-your-app-from-react-components-to-hooks-46c0ffea1dc6

 

이 글에서 잘 설명이 되어 있는데 훅은 stateful logic 을 컴포넌트로 부터 분리하는 기능을 제공한다고 되어 있고

과거에는 중첩된 higher-order components, providers, render props 들을 통해 우회적으로 구현하였던 것들을 훅을 통해 간결하고 올바르게 구현할 수 있게 되었다. 훅이 아닌 과거의 방법대로 구현을 하면 이들을 한꺼번에 사용하게 되었을 때 코드가 굉장히 복잡해지고 더러워질 수 밖에 없었다고 한다.

 

반응형

댓글

Designed by JB FACTORY