리그오브레전드 매칭 분석 서비스 <뉴메타> 회고

devPomme·2021년 5월 4일
12
post-thumbnail

프로젝트 요약

프로젝트 기획 단계

매칭 알고리즘 구상

롤 랭크게임을 할 때마다 정글러든 라이너든, "ㅈㄱㅊㅇ" 때문에 고통받는 사람들이 많은 것 같았다. 예를 들어, 탑 라이너 입장에서는 우리팀 정글러가 3렙 갱을 오는 지 안 오는 지만 알아도 라인 관리하기가 편할 거 같지 않은가? 정글러 입장에서는, 매칭된 탑 라이너의 최근 플레이한 챔피언이 티모, 베인, 루시안이라는 걸 안다면 바로 깔끔하게 닷지할 수 있을 것이다.

이머시브 코스 전 프리코스때부터 나는 이 아이디어를 꼭 구현해보고싶었고, 페어 프로그래밍하는 동기들한테 사방팔방 다 떠들고 다녔다. 저 무조건 이거 할거예요, 라고.

  1. 정글러와 라이너의 매칭 궁합을 확인해본다는 게 기획 단계의 골자였다. 그래서 라인전 단계인 게임 시작 후 15분동안의 데이터만 가지고 정글러/라이너의 플레이스타일을 분석할 지표를 정하기로 했다.

  2. 메인 포지션이 정글러일 경우 15분 동안 2렙/3렙 싸움(갱) 적극적/소극적/갱 회피형 등으로 나누고, 라이너의 경우엔 15분 기준 KDA 지표(라인전 강함/보통/약함)에 따라 점수를 차등적으로 부여하고, 두 플레이어의 점수 합에 따라 매칭 점수가 산출되도록 설계했다.

그 외에 다른 플레이스타일(용/전령 잘 챙김, 2렙/3렙때 잘 죽음 or 잘 죽임 등) 은 키워드 태그를 통해 직관적으로 표현하기로 했다.

초기 단계에서는 포지션 좌표 값까지 확인할 수 있으니, 플레이어들의 동선에 따라 어느 라인에 집중하는 스타일인지도 보여주고 싶었지만, 나 혼자서 이 모든 기능들을 구현할 수 없다는 걸 깨닫고, 과감히 포기했다.

검증을 위한 리서치

데이터 수치를 너무 낮거나 높게 잡아도 별로 실효성이 없을테니까, LCK 챌린저스 리그 운영을 하고 계신 분에게 자문을 구하기도 했다.

기술 스택

TypeScript

엔지니어분들이 꼭 한 번 써보라고 추천하셔서, 일단 해야하는구나! 하고 덥썩 배우게 되었다. 쓰다보니 확실히, 정적으로 타입을 지정해주면서 개발자가 작성한 코드의 의도가 명확해지는 것이 좋았다. 만약에 내 코드를 다른 사람이 유지보수를 하게 되더라도, 코드를 이해하는 데 드는 리소스가 훨씬 절약되겠구나 싶었다.

GraphQL을 못 써본 아쉬움

사실 GraphQL을 적극적으로 써보자고 팀에 어필을 했던 사람은 바로 나였다. 하나의 엔드포인트에 요청하는 것만으로 필요에 따라 원하는 데이터를 받아쓸 수 있게 되면서, 리덕스를 거의 쓸 필요가 없을 정도로 상태관리가 쉬워진다는 점에서 엄청난 메리트를 느꼈기때문이다.

특히 나는, 게이머의 정보를 조회할 때마다 DB에 업데이트를 하고, 그 DB에 있는 데이터를 클라이언트가 쿼리문을 통해 요청하는 것을 생각했었다.

그 러 나
팀원들과의 토론을 한 결과 게이머들의 LOL 데이터는 DB에 저장하지 않는 것으로 결론이 났다. 열심히 다른 분들이 클론코딩한 레퍼런스를 찾아보면서, 40명 정도의 게임 데이터가 약 7MB정도인 것으로 확인을 했는데, 그래도 팀원들에게는 확실히 부담의 요인이 되었던 것같다.
그래서 내가 담당하는 데이터 분석 기능은 DB를 사용하지 않고 구현하는 것으로 결정되었다.

또한 한 번의 서버 요청으로 게이머의 모든 데이터를 받기만 하면 되고, 유동적으로 response를 받을 필요가 없었기때문에, 열심히 GraphQL 강의도 듣고 공부도 했지만, 결과적으로는 RESTful API 방식을 더 깊이 이해하게 되었다.

어찌 되었든, GraphQL과 RESTful API 방식의 사용 목적과 차이점에 대해서 더 확실히 알 수 있게 된 계기였기때문에 결과적으로는 만족하는 편이다.

React

나는 이번 프로젝트에서 사실상 지역 상태 관리만 했기때문에, react-hooks, react-router-dom으로 모든 걸 다 해결했다. 데이터 분석 페이지에서는 여러 개의 그래프 컴포넌트들밖에 없었고, 한 뎁스 이상으로 props를 내려줄 필요가 없었기때문이다.

Redux가 그립...진 않았다.ㅋㅋㅋ

Redux가 복잡하긴 하지만 장점도 있다는 것도 안다. (타임 트래블 디버깅, 상태 프리징 / 직관적인 데브툴 등) 하지만 필연적으로 리덕스는 보일러 플레이팅이 너무 심하다고 생각했다.

물론 나는 Redux-saga를 써본 적이 없었기때문에 네트워크 및 데이터 처리 관련 로직을 분리시켜서 Redux를 쓴 적이 없었고, 만약에 saga를 썼다면 또 이렇게까지 불만이 없었을지도 모른다.

결론적으로, 구직활동을 하면서 redux를 대체할 수 있는 다른 상태관리 라이브러리를 배워보고 싶다는 생각이 많이 들었다. React 16부터 기본 제공되는 Context API나 Recoil, Jotai ... 굉장히 많더라고. 으하하.

뭔가 쓰다보니 리덕스 얘기를 굉장히 많이 한 것같다. 첫번째 프로젝트에서 리덕스때문에 고생을 많이 했다보니, 파이널 프로젝트에서는 리액트 환경에서 전역 상태 관리에 대한 고민과 관심이 굉장히 커졌었다.

백엔드

Riot Games API 요청하기

클라이언트 소환사의 닉네임을 입력했을 때, 서버는 그 닉네임을 가지고 라이엇에 API를 요청한다. 클라이언트에서는 직접 라이엇 API에 요청할 수 없기 때문에 (CORS 에러가 뜸) 서버 게이트웨이 방식으로 구현했다.
다음은 검색창에 프로게이머 페이커의 소환사명 hide on bush 를 입력했을 때 서버가 응답하는 데이터이다. (굉-장히 길어서 좀 줄였다.)

"summonerInfo": {
        "id": "2n-VXkaE0WoQT45fGVnu5ctMvh8V9tJ5u3UbOsPpcxiY7A",
        "accountId": "yjgY91pXbK3m9uD1BGI3DrzysSOiNaXnltwa-XzNYxhX",
        "puuid": "dFxLz9CDvgTrTJsvqW7fnjc7Rnhlx8BHROIh757EViOSvIL3mQksNYA40Mxm9JeHgPpaZnEPfX7DZw",
        "name": "Hide on bush",
        "profileIconId": 6,
        "revisionDate": 1618917644125,
        "summonerLevel": 441
    },

"leagueInfo": {
        "leagueId": "dbb721c8-b8fa-3d0c-9727-bfa4eabefc30",
        "queueType": "RANKED_SOLO_5x5",
        "tier": "MASTER",
        "rank": "I",
        "summonerId": "2n-VXkaE0WoQT45fGVnu5ctMvh8V9tJ5u3UbOsPpcxiY7A",
        "summonerName": "Hide on bush",
        "leaguePoints": 37,
        "wins": 336,
        "losses": 304,
        "veteran": false,
        "inactive": false,
        "freshBlood": true,
        "hotStreak": false
    },

    "laneInfo": {
        "TOP": 6,
        "JUNGLE": 2,
        "MID": 60,
        "AD_CARRY": 10,
        "SUPPORT": 22
    }

 "recentMatches": [
        {
            "platformId": "KR",
            "gameId": 5145154286,
            "champion": 236,
            "queue": 420,
            "season": 13,
            "timestamp": 1619023595169,
            "role": "SOLO",
            "lane": "MID"
        }, 
   ...],
    "kdaTimelineData": [
        {
            "matchKills": 1,
            "matchAssists": 0,
            "matchDeaths": 0,
            "matchDragonKills": 0,
            "matchHeraldKills": 0,
            "matchKillForLevel3": 0,
            "matchAssistForLevel3": 0,
            "matchDeathForLevel3": 0,
            "matchKillForLevel2": 0,
            "matchAssistForLevel2": 0,
            "matchDeathForLevel2": 0
        },...],
"expTimelineData": [
        [
            {
                "timestamp": 60028,
                "participantFrames": {
                    "participantId": 6,
                    "position": {
                        "x": 4643,
                        "y": 11988
                    },
                    "currentGold": 0,
                    "totalGold": 500,
                    "level": 1,
                    "xp": 0,
                    "minionsKilled": 0,
                    "jungleMinionsKilled": 0,
                    "dominionScore": 0,
                    "teamScore": 0
                }
            },
  • summonerInfo: API 요청을 하기 위한 암호화된 아이디 정보
  • leagueInfo: 랭크 게임 점수 및 티어 정보
  • laneInfo: 최근 플레이한 경기(최대 100전까지) 기준 플레이한 포지션별 게임 횟수
  • recentMatches: 최근 플레이한 20전의 챔피언 정보
  • kdaTimelineData: 15분까지의 KDA 지표 및 오브젝트 획득 횟수(20경기)
  • expTimelineData: (20경기의) 15분까지의 분당 획득 골드량, 미니언 경험치 획득량, 해당 시간대 위치값 등이 담겨있는 데이터

이 모든 데이터들이 한 번의 요청으로 받아진다.

라이엇의 제품 개발 키는 limit rate 가 굉장히 빡세다.

20 requests every 1 second
100 requests every 2 minutes

"이걸로 엌덬케 개발을 하라고!!" 라는 말이 저절로 나왔지만 별 수 있나. 주는 대로 해야지.

여기서부터 비동기 지옥이 시작되었다.

비동기 마스터(가 되고싶었다)

then을 정말 아주 많이 남발했다. 사실 await/async를 쓰면 더 간결해진다는 걸 머릿속에서는 알고 있어도, 나한테는 선행된 코드의 결과값이 매개변수로 바로 들어오는 then이 더 직관적이었다. 그러다보니 코드의 depth가 깊어졌고, 팀장님의 딜이라고 쓰고 사랑의 회초리라고 읽는다이 들어왔다..

프로젝트 하기 전까지 이렇게 비동기 코드를 많이 써본 적도 없었고, 코드 실행 순서에 대한 명확한 이해를 하지 못한 상태에서 코드를 작성해서 생겼던 것같다.

그래도 팀장님의 코드 리뷰를 받으면서
Promise.all 도 써보고, 불필요하게 작성된 then도 지우고, async/await도 써보면서 비동기 처리에 대한 이해를 높일 수 있었던 것이 가장 큰 보람이었던 것같다.

429 429 429...

결국 혼자서 깔끔하게 429 에러를 해결하지는 못했다.

내가 프론트엔드에서 고생하는 동안 팀장님은 코드가 실행될 때마다 API 리밋을 카운트하면서 초당 20개 요청을 초과했을 때는 때는 setTimeoutlater를 적절히 써가며 429 에러가 뜨지 않도록 했다.

그럼에도 불구하고 듀오 매치를 검색할 때는 2분 정도 쉬었다가 검색을 시도해야 429 에러를 피할 수 있었다. (아마 2분이 지나기 전에 다시 요청을 날리면 100회 리밋에 걸렸기때문이겠지...)

프론트엔드

자! 이제 프론트엔드다!

데이터 시각화

처음에는 라이브러리 nivo를 쓰려고 했다.
그러나 치명적인 단점들을 발견하게 되었다.
1. combined charts가 제공되지 않는다.
2. 그래프마다 패키지를 깔아야한다.

3. peer dependency error에 부딪혔다.

3번 이슈가 나에게 제일 부담이 컸다. 1차는 타입스크립트 및 GraphQL을 공부했고, 2주차부터 3주차 중간까지 백엔드에서 컨트롤러 작업에 시간을 쏟았고, 3주차부터 시각화를 해야하는데 도저히 새로운 라이브러리를 다시 배우고, 개발환경 세팅에 시간을 쏟을 엄두가 나지않았다. nivo의 그래프 디자인은 너무 예뻤고, 커스터마이징 기능 역시 매력적이었지만 눈물을 머금고 다른 라이브러리로 갈아탔다.

Apexchart

에이펙스차트는, nivo만큼 예쁘진 않았지만 ㅋㅋ 공식문서는 그럭저럭 친절했고, 인터페이스 정의가 잘 되어있어서 어떻게 그래프를 커스터마이징해야할 지 확인을 할 수 있었다.
물론 당연히 쉽진 않았지만, 어 이 부분은 커스터마이징 가능할 거 같은데? 싶어서 조금만 더 뒤져보면 바로 나왔다는 점에서 만족했다. 세세하게 디자인할 수 있는 기능들이 너무 많다.
무엇보다 반응형도 직접 그래프 컴포넌트에서 작업이 가능해서 좋았다.

반응형

해보기 전까진 겁을 많이 먹었는데, 미디어쿼리를 쓰는 것이 재미있었다. 기획 단계에서 모바일 뷰를 조금 더 고려하지 못한 게 실수였던 것같고, 조금 더 시간이 있었다면 더 깔끔하게 할 수 있었을텐데 라는 아쉬움이 많이 들었다.

Sass

너무 편했다. 뭐 말할 것도 없다. 변수 지정이 가능하니까 코드 중복도 피할 수 있고, 유지보수도 편해진다는 게 확실히 느꼈다. 김버그님의 강의를 결제했는데 조금 더 효율적으로 Sass를 쓰는 방법을 훈련해야겠다는 생각이 들었다.

프로젝트를 마무리하며

사실 하면서 이것보다 더 많은 걸 해내야하지 않나? 라는 강박관념에 계속 시달렸다. 나에 비해서 다른 분들이 훨씬 더 많은 걸 해내고 있는 거 같은데, 내 실력은 이것밖에 안 되는 것같다고 스스로를 자책했다.
그리고 나는 완벽주의적인 강박이 강하다는 걸, 원래 알고 있긴 했지만 프로젝트라는 촉박한 일정 속에서 보여주는 게 심적으로 힘들었다.
나 자신을 평가절하하게 되고, 내가 새롭게 배우고 얻은 경험을 과소평가하는 부정적 경향이 강해지자, 팀장님은 성장하기 위해서 그런 식의 관점은 바람직하지 않다는 걸 피드백 해주었다. 덕분에 다시 정신을 차리고 프로젝트 마무리까지 힘낼 수 있었다.

리팩토링 과제

  • 데이터 요청 및 가공 컨트롤러 리팩토링
    : 라이엇 게임즈는 빨리 제품키를 달라! setTimeout을 지워버리고 싶다...
  • KDA 히트맵 그래프 리디자인
    : 솔로 검색 기준으로 지금은 hover 했을 때 아무 것도 뜨지 않지만, 해당 게임에서 플레이한 챔피언 정보와 승패 결과까지 보여주면 좋겠다. 혹은 16경기가 아니라 이번 시즌 플레이한 경기 데이터 모두 띄워주는 히트맵으로 다시 구현하고 싶다. (Github 잔디처럼 ㅎㅎㅎ)
profile
헌신하고 확장하는 삶

8개의 댓글

comment-user-thumbnail
2021년 5월 4일

비동기 마스터 짤ㅋㅋㅋㅋㅋㅋㅋ
아니근데 진짜 저랑 같은 코스들은거 맞나 싶을 정도로 퀄이 달라버리네요.... 😇 😇 😇 😇😇
놀라운일....ㅋㅋㅋ
모쪼록 수고 많았음다 ㅠㅠㅠㅠㅠ 이제 저희 취준뿌셔ㅠㅠㅠ

1개의 답글
comment-user-thumbnail
2021년 5월 4일

꼭 하고싶어했던 아이디어를 프로젝트로 완성하는 모습이 멋있어요. 글에서도 스스로 되돌아보고 발전하는게 느껴져서 꼭 뛰어난 프로그래머가 될 거라고 믿고 기대하고 있습니다. 화이팅!

1개의 답글
comment-user-thumbnail
2021년 5월 4일

잘봤습니다~

1개의 답글
comment-user-thumbnail
2021년 5월 17일

정말 존경스럽고 대단하시네요..ㅋㅋㅋㅋㅋ

1개의 답글