brunch

You can make anything
by writing

C.S.Lewis

by 강민혁 Oct 03. 2022

#1 use-upbit-api에 관해서

Upbit Websocket API - React Custom Hook

요즘 마음이 약간 들뜨고 정리가 안되어서 공부도 손에 잘 안 잡힌 마당에, 지난 프로젝트들에 대한 고찰을 해보려 한다.


근 2달 전 나는 직접 제작한 npm 패키지를 배포했다.


쉽게 이야기하면, 오픈 소스를 개발하여 많은 사람들이 쉽게 다운로드하여 사용할 수 있게 만들었다.


이 프로젝트는 무엇인가?

먼저 이하 링크를 통해 이 프로젝트에 대해 자세히 살펴볼 수 있다.

npm link

github link


demo website link

이 프로젝트는 Upbit Websocket API를 사용하는 React 개발자들을 위해 만들어진 오픈소스이다.

쉽게 설명하면 코인 거래소인 Upbit의 실시간 시세, 거래 데이터, 호가 정보를 코드 한 줄로 쉽게 받아올 수 있도록 설계된 npm 패키지이다. 이 패키지를 다운로드하여 사용하면, 복잡한 처리 과정을 거치지 않고도 원하는 대로 Upbit 실시간 정보를 수신하는 기능을 구현할 수 있다.


왜 이 프로젝트를 시작했는가?

사실 처음부터 이 프로젝트를 계획하고 시작했던 것은 아니다.

이 프로젝트의 시작은 오로지 "내가 쓰기 위해서" 시작했다.

하지만 막상 핵심 기능들을 구현하는 과정에서, "나만 이 문제를 겪는 것이 아닐 것"이라고 확신했다.

 특히 이 문제를 겪는 많은 사람들이 나의 코드를 통해 더 빠르게 비즈니스 로직을 구현할 수 있다면, 굉장한 보람을 느낄 것이고, 나에게 있어서도 좋은 경험이자 결과물이 될 것이라 생각하여 프로젝트를 오픈소스 화했다.


나는 예전에 코인 트레이딩을 본격적으로 해본 경험이 있다. 그 과정에서 사실 많은 자금을 잃었고, 현재까지도 복구하지 못했다. 그 경험 이후, 나는 내가 직접 모니터 앞에 앉아 트레이딩 하는 것보다 차라리 알고리즘을 만들어서 트레이딩을 하는 편이 더 효율적이라고 생각하게 되었다. 일단 트레이딩 과정에서 겪는 정식적 고통이 생각보다 일상생활에 많은 지장을 줬고, 이로 인해 정상적인 일상생활을 할 수 없었기 때문이다.


그래서 제일 먼저 시도했던 것은 Upbit의 시세 정보를 가져와서 내가 만든 알고리즘 검색기로 매수할 만한 코인을 자동으로 찾는 프로그램을 만드는 것이었다. 결론부터 이야기하자면, 그 프로젝트는 실패했고 완성된 프로그램으로 발전시키지 못했다.


여러 실패 요인이 있었지만, 위 실패의 가장 큰 이유 중 하나는 바로 Websocket API를 활용한 시세 수신에 있었다. 생각했던 것보다 Websocket API를 이용해서 시세를 수신하는 것이 내가 원하는 대로 작동하지 않아서 어려움을 겪었는데, 이것이 다른 문제들과 함께 찾아오니 이 프로젝트가 너무나 버겁게 느껴졌다. 계획한 기능은 산더미인데, 프로젝트 규모가 조금씩 커질수록 문제들은 기하급수적으로 늘어났다. 근본적인 문제는 실력 부족이라는 것을 알았지만, 복학을 앞둔 시점에서 더 이상 프로젝트에 시간을 쏟기 어려워졌다. 그렇게 아쉽지만 프로젝트 진행을 접고 4개월 이후에, 여름 방학이 찾아왔다.


지난 실패의 요인을 "내 실력에 비해, 스프린트 목표를 너무 높게 잡았기 때문"이라고 진단하고, 간단하지만 중요한 문제를 먼저 해결하는 방식으로, 문제에 대한 접근 방식을 바꿨다. 


이때, 나는 지난 실패를 교훈 삼아, "스프린트 목표를 1주일 내에 실행 가능한 것으로 설정할 것"이라는 방식을 차용했다.


그래서 그리고 이후에는 이 원칙에 따라 빠르게 내가 원하는 기능을 구현할 수 있었다. 그렇게 내가 생각한 대로 원하는 기능이 구현되고 잘 작동하게 되었다. 사실 이제 그럼 내가 이 기술을 가지고 이전에 실패했던 프로젝트를 진행하면 되는 것이었지만, 오히려 나는 이 프로젝트를 진행하면서 이 문제를 겪는 다른 사람들이 떠올랐다.


어찌어찌 구현은 했지만, 생각보다 많은 로직을 필요로 하는 구현이었고, 이 구현을 다른 사람들이 쉽게 할 것이라고 생각하지 않았다. 심지어 실력 있는 개발자라 할지라도, 스트림 데이터로 많게는 1초에 수백 건씩 송신되는 이 데이터들을 다루기 위해서는 귀찮은 작업들이 많이 필요할 터였다.


그리고 나 또한 이 기술을 좀 더 체계적으로 모듈화 시켜서 관리하고 싶은 욕심이 있었기 때문에, 본격적으로 오픈소스 화하기 시작했다. 처음으로 npm 패키지를 배포해보는 터라, 자잘한 어려움들이 있었지만 핵심 기능을 구현해놨기 때문에 어렵지 않게 배포할 코드를 만들 수 있었다. 오히려 어려웠던 것은 개발 문서 작성이었는데, 이는 뒤에서 자세히 다루겠다.


결론적으로, 이 프로젝트는 "내가 쓰려고 만들었는데", "남들도 쓰면 좋을 것 같아서" 만든 프로젝트이다. 사실 그렇게 대단한 사명감이나 이유가 있어서 만든 프로젝트는 아니다. 하지만 지금은 꽤나 보람을 느끼고 있는 프로젝트이고, 이 프로젝트 덕분에 실력 있는 개발자들과 대화할 수 있는 기회도 가질 수 있었다.


어떤 어려움이 있었는가?


2달이 지난 시점에서, 자잘한 문제들은 잘 기억나지 않아 큼직한 Challenge point들만 적어보려 한다.


먼저 Upbit API 자체에 친숙해지기 위해, Upbit REST API를 사용한 예제 사이트를 만들었다. 이 부분은 큰 어려움이 없었기 때문에, 빠른 시간 안에 완료할 수 있었고 다음 스텝으로 넘어갈 수 있는 준비도 마쳤다. 이후에 본격적으로 Websocket API 자체를 탐구하기 시작했다.


1. Socket Control

React에서 Websocket API를 사용하면, socket의 상태 관리라는 문제를 마주치게 된다. React 컴포넌트 외부에서 socket을 관리하면 사실 해결되는 문제이긴 했다. 하지만 React 컴포넌트에서 socket을 컨트롤할 수 없다면, 기능 확장이나, 소켓 연결 및 해제, 메모리 누수 등에 대한 대응이 어렵기 때문에, React 컴포넌트에서 socket을 컨트롤할 수 있게 만들고 싶었다.


사실 이 부분은 프로젝트 진행 전반에서 계속 문제가 있었던 부분이다. 해결한 줄 알았는데, 새로운 기능을 추가하니 새로운 문제가 생기고 예상치 못한 상황에서의 문제가 빈번하게 발생했다. 하루면 해결할 수 있을 거라 예상했던 기능이 3일이 걸렸다. (이 순간 이후로, 새로운 스프린트를 정하면 구현이 필요한 핵심 기능들을 먼저 아주 간단하게 구현해본다. 일단 핵심 기능이 내가 생각한 정도의 수준으로 난이도를 갖고 있는지 확인하기 위함이다.)


결국 이 문제에서 중요한 것은 '소켓을 연결을 명령하는 시점'과 '소켓에 이벤트를 달아주는 시점', '소켓에 필요한 연결 시간', '적절한 cleanUp 함수의 사용으로 소켓의 연결을 해제'하는 것들이 핵심 요소였다.


먼저 컴포넌트가 마운트 되는 순간에 useRef를 활용하여 socket 객체를 담을 장소를 확보하고, useEffect에서 소켓을 연결하고 이벤트를 달아주었다. 그리고 useEffect에서 선언했으니, 컴포넌트가 언마운트되는 시점에 작동하는 cleanUp 함수로 소켓 연결을 해제해주면서 소켓 연결이 컴포넌트가 사라진 뒤에도 남아있는 문제를 해결했다.


또 소켓의 open 이벤트에서, 연결되지 않은 상태에서 message를 보내는 상황이 부분적으로 발생했다. 이게 항상 발생하는 문제가 아니라, 인터넷 통신 환경에 따라서 접속이 늦어지면 문제가 생기는 터라, 이를 위해 socket의 ready state를 확인하고 메시지를 보내는 방식으로 방어적으로 프로그래밍했다.


이 과정에서 useEffect의 동작 원리를 더 제대로 파고들게 되었고, 덤으로 Websocket API 문서를 수십 번을 읽어보게 되었다. 사실 Websocket API를 사용하면서 가장 어려웠던 것은, 동일한 상황에서도 어떠한 오류가 발생하기도 하고 아니기도 했다는 것이다. 그래서 디버깅이 어려웠고, 해결하는데 예상보다 많은 시간을 요했다.


2. Stream Data Control

두 번째로 문제였던 것은 Websocket으로 넘어오는 데이터 처리였다.


사실 이 부분이 가장 어렵지 않았나 싶다.


일단 문제가 되었던 것은 Upbit Websocket API에 하나의 코인에 대한 요청을 보내면 큰 문제가 발생하지 않는다. 하지만 여러 코인에 대한 요청을 보내면, 스트림으로 제멋대로 데이터가 들어온다. 또 그 양도 상당히 많아서 데이터가 업데이트될 때마다 상태를 업데이트하면, 컴포넌트가 re-rendering 될 것이고, 성능 이슈 때문에, 이는 웹사이트의 과부하로 이어진다. 특히 10개 이상의 코인에 대해 요청을 보내면 수신되는 데이터의 양이 너무도 많아서 이는 컴포넌트 최적화 정도로 해결할 수 있는 문제는 아니었다.


그렇다면 코인 별로 소켓 연결을 다르게 할 수 있지 않냐고 생각할 수 있지만, Upbit Websocket API는 소켓 연결 제한이 있기 때문에, 2-3개라면 가능할지 몰라도 그 이상의 코인을 서로 다른 socket으로 연결하는 것은 불가능할뿐더러, 그렇게 구현하는 것은 굉장히 비효율적이라고 생각했다. 업비트에 상장된 코인만 수백 개인데, 이를 각각 독립적인 socket으로 연결하는 것은 유효한 방법은 아니라고 생각했다.


그래서 생각해낸 방법은 의도적으로 쓰로틀링을 걸고 버퍼에 데이터를 쌓아두는 것이었다. 먼저 buffer를 만들고 거기에 수신되는 모든 데이터를 저장했다. 그리고 lodash library의 throttle 함수를 활용하여, 지정된 시간 동안 버퍼에 데이터를 저장시키고 난 뒤에는 데이터 처리 로직을 거쳐 유효한 데이터만 추출하고 버퍼를 비우는 방식을 사용했다.


실시간 데이터였기 때문에, 같은 코인에 대한 정보가 스트림으로 수신되는 것이었고, 사용자에게는 마지막으로 수신된 결과만 유효하기 때문에, 각 코인 별로 마지막으로 수신된 데이터만을 추출하고, 이후에는 그 데이터들을 사용자가 요청한 코인의 순서대로 재 정렬한 뒤에 loading buffer라는 곳에 다시 저장했다.


바로 정제된 데이터를 return하지 않고, loading buffer로 보낸 이유는 다음과 같다.


물론 처음 요청 시에는 스냅샷으로 요청 코인 전체에 대한 정보를 수신할 수 있지만, 이후의 새로운 데이터는 요청한 모든 코인에 대한 데이터를 새로 업데이트하지 않는다. upbit api에서 일방적으로 시세가 변동된 코인이나, 거래가 발생한 코인에 대한 데이터를 송신하기 때문에, 어떤 코인은 5초 동안 데이터가 수신되지 않기도 한다.


loading buffer는 기존의 데이터 상태와 비교 한 뒤 업데이트된 코인 데이터만 새로운 데이터로 치환하여 return 했다. 이 return 된 값은 최종적으로 사용하는 상태 데이터가 된다.


이렇게 하여 상태 변경을 의도적으로 쓰로틀링 하여 내가 원하는 time interval로 컨트롤할 수 있게 되었다. 


데이터 처리 알고리즘은 내가 직접 만들어서 사용했고, 이는 이전의 python으로 워낙 머신러닝 코드를 작성하면서 데이터 배열을 많이 다루다 보니 비교적 수월하게 해결할 수 있었던 부분이었다. 오히려 버퍼라는 아이디어를 떠올리고, throttle이라는 개념을 떠올리고 구현하는 것이 가장 병목이 되었던 파트였다.



3. example website 제작에서 의도적 기술적 제한

Upbit Websocket API를 컨트롤하는 리액트 훅을 제작한 후에는, 이를 사용한 예시 웹사이트를 제작했다. 내 라이브러리를 사용하는 사람들이, 이 라이브러리로 어떤 것을 할 수 있는지, 어떻게 사용하는지 실제 예시를 보고 싶을 것이라 생각했기 때문이다. 또 내 경험상 예제 코드가 없는 라이브러리는 사용하는데 어려움을 많이 겪었다. 적어도 내가 만든 라이브러리의 유저는 그런 사용자 경험을 겪지 않았으면 했다.


그리고 이 웹사이트를 제작할 때, 최대한 쉽게 사용자들이 코드를 읽을 수 있도록 순수 React만으로 구현했다. 핵심 코드를 간편하게 볼 수 있도록 typescript도 사용하지 않았고, recoil과 같은 상태 관리 라이브러리도 제한하고, TOTAL example을 제외한 독립 예제에서는 CSS 없이 개발했다.


그리고 각 페이지에 대한 컴포넌트 코드를 한눈에 보게 하기 위해, 의도적으로 각 컴포넌트들을 추상화하지 않고 구현했다. 유저들이 추상화된 코드를 일일이 찾아다니면서 보게 하고 싶지는 않았기 때문이다.


이 과정에서 익숙히 쓰던 기술을 일부러 쓰지 않고, 코드의 추상화 없이 구현하다 보니 역으로 typescript와 recoil이 너무나 그리워지기도 했다.


하지만 무엇보다 유저의 입장에서 제품을 바라보는 것이 중요하다고 생각하기 때문에, 추후 typescript 버전을 추가하는 일이 있더라도, 일단은 typescript를 사용하지 않는 사람도 읽을 수 있는 코드를 보여주고 싶었다. 그리고 recoil을 사용하지 않는 사람도 쉽게 코드를 읽고 이해할 수 있도록 만들고 싶었다.


4. 개발 문서 작성

개발 문서를 작성하는 과정은 프로그래밍을 시작한 뒤로 처음 시도하는 일이었다. 하지만 평소 글로써 무엇인가를 정리해두고 설명하는 것을 즐겼기 때문에, 걱정은 안 했다.


하지만 실제로 작성해보니 어려운 것보다는 귀찮음이 더 강했다. 일일이 모든 내용에 대해 기술하고, 예제를 만드는 것이 귀찮았다. 그래서 생각보다 늘어졌고, 2-3일이 더 소요되었지만, 나름 충분한 설명을 담았다고 생각한다.


무엇을 얻고 배웠는가?


일단 이 프로젝트를 완성하면서, 난생처음으로 내가 독립적으로 오픈 소스를 만들었다는 성취감을 얻을 수 있었다. 그리고 첫 주에 500명 이상이 다운로드하는 반응을 보며 개인적인 인정 욕구도 조금은 해소가 되었다.(사실 그 후에는 지속적으로 다운로드 수가 줄고 있다.)


그리고 흥미로웠던 점은 어떤 프로젝트를 처음부터 배포까지 끝까지 완성해보고 나니, 다음 프로젝트도 빨리 진행하고 싶다는 설렘과 자신감이 솟기 시작했다. 이후에는 실제 서비스나 제품을 만들고 싶다고 생각했다. (실제로 trading game app을 만드는 것을 실행했다. - 현재 데모 버전 운영 중)


또 각종 개발자 커뮤니티에 홍보하며, 사소한 피드백을 듣기도 하고, UNIST에 있는 개발자 선배나 동기의 연락을 받기도 했다.


 프로젝트 하나로  다음 모든 프로세스가 시작되었고, 새로운 자극과 함께 나에게 추진력을 불어넣어 줬다. 추후에 시간이  ,  오픈소스의 부족한 부분을 업데이트하려 한다.


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari