싸피 2학기 첫 프로젝트였던 집에서 운동중(줄여서 집중) 프로젝트 회고 글이다.
https://github.com/camiyoung/ZipZong
GitHub - camiyoung/ZipZong: 머신러닝을 활용한 화상 채팅 운동 플랫폼 | 🏆 SSAFY 공통 1등 수상
머신러닝을 활용한 화상 채팅 운동 플랫폼 | 🏆 SSAFY 공통 1등 수상. Contribute to camiyoung/ZipZong development by creating an account on GitHub.
github.com
화상 채팅방에서 모두 다함께 운동을 할 수 있는 서비스를 제공한다.
구글의 Teachable Machine으로 제작한 머신러닝 모델을 활용하여 현재 진행중인 동작의 수행 횟수를 자동으로 카운트해준다.
이 글에서는 내가 제작했던 화상채팅 + 운동 동작 측정부분에 관해서만 작성할 예정이다.
기능 소개
화상채팅방에서 운동 시작, 동작 개수 측정, 종료까지 아래 사진에보이는 기능들을 제작했다.
- 동작(위 사진에선 스쿼트)을 1개 성공하면 카운트가 1 증가한다.
- 사전에 등록된 운동 진행 순서와 현재 진행중인 동작을 보여준다.
운동 진행 순서는 운동방(채팅방)을 개설하기 전에 생성할 수 있다. 운동 시간은 1분고정, 휴식시간은 생성하는 사람이 조절할 수 있다.
운동을 시작하면 방장이 선택한 운동 목록을 참여자 모두가 실행한다.
- 남은 운동시간, 휴식시간을 보여주는 카운트를 제공한다.
- 그 외 : 채팅 , 시작, 종료 등 채팅방에 관련된 기능
어려웠던 점
데이터 상태 관리
유저가 채팅방에 입장하면 갖고있어야 하는 데이터는 다음과 같다.
1. 진행할 운동 목록
방장이 진행할 루틴을 변경해야 할 때 마다 참가중인 모든 유저의 목록이 변경되어야 한다.
2. 현재 진행중인 운동 동작
한 동작을 정해진 시간만큼 진행한 후 다음 동작으로 바뀌어야 한다.
3. 현재 진행중인 운동 동작의 수행 횟수
현재 수행중인 동작이라면 유저가 성공한 횟수를 저장해야 한다.
화상 채팅방내에는 많은 기능이 있어서 최대한 상호 영향을 끼치지 않도록 컴포넌트를 작게 분리했다.
이러한 상황에서 계속해서 실시간으로 변경되는 데이터 관리를 하는것이 꽤나 까다로웠다.
운동방의 컴포넌트 구조는 다음과 같다. 단순 정보만 불러오는 OtherPeople이나 Chat은 하위 컴포넌트를 생략했다.
여기서 상태관리가 필요한 컴포넌트를 크게 정리해보면 다음과 같다.
간략하게 정리해도 여러개의 컴포넌트에서 상태를 알고 있어야 한다. 처음에는 상위 컴포넌트에서 하위로 state를 전달하는 방식으로 개발을 진행했다. 그러나 컴포넌트가 많아지고 깊어질수록 props drilling 현상을 너무 체감할 수 있었고 redux를 사용해서 전역으로 상태를 관리하도록 했다.
진행할 운동 루틴은 exercise zone, side bar 둘 다 알고있어야 한다.
또한 현재 진행중인 단일 운동은 exercise info와 todolist에서 알고 있어야 한다.
따라서 redux를 사용하여 운동 리스트와 현재 진행중인 운동의 index를 저장하였고, 이것들이 변경 될 때 마다 원하는 동작을 수행시켜주었다.
접속 유저들간의 이벤트 송수신 딜레이
화상채팅방에 접속한 유저들이 모두 운동을 동시에 시작해야했고, 끝나면 방에 있는 모든 유저의 결과를 취합하여 DB에 저장한 후 결과 페이지에 보여주어야 한다.
방장만이 운동을 시작할 수 있으므로 방장이 시작버튼을 누르면 존재하는 모든 인원에게 운동 시작 이벤트를 전송한다.
각 유저들은 운동을 수행하고 본인의 기록을 방장에게 종료 이벤트로 전송한다.
유저간 이벤트를 수신하는 딜레이가 발생했다. 운동을 시작하는 방장이 시작 이벤트를 모두에게 전송후 동시에 운동을 시작하도록 해야 했는데 네트워크 상황에 따라 어떤이는 늦게 수신하는 경우가 있었다. 혹은 같은 시간에 시작을 하더라도 방장이 특정 유저의 종료 이벤트를 늦게 수신하는 경우 해당 유저의 결과가 누락되는 경우가 발생했다.
이를 해결하기 위해서 1차적으로 방장이 종료이벤트를 수신할 때, 참여인원수 만큼 이벤트를 수신하여야먄 온전히 운동이 끝났다고 판단하는 로직을 추가 했다. 예를들어 방장 포함 5명이 운동에 참여했다면, 운동이 종료되었다는 이벤트를 5개를 수신해야 온전한 종료라고 판단하고자 했다. 그러나 정말 네트워크의 문제상으로 운동 도중 연결이 끊긴 유저가 발생한다면 평생 운동이 종료되지 않는 문제가 발생한다. 이를 해결하기 위해서 인원수 만큼의 이벤트를 수신하거나 최초 이벤트 수신 후 (방장이 운동 종료할 때 발생) 10초가 지나면 운동을 종료시키도록 하였다.
운동이 종료되면 방장은 백엔드에 모든 유저의 운동 결과를 전송한다. 백엔드에서는 유저 운동 정보를 DB에 저장하고 해당 유저들의 달성률을 취합하여 다시 모든 유저들에게 이벤트로 전송한다.
티처블 머신과 리액트 접목하기
이 프로젝트를 하며 가장 까다로웠던 부분이다. 티처블머신을 활용하는 기본적인 예제는 존재했으나 리액트에서의 예시는 별로 없었다. 거기다가 화상 채팅방에 사용되는 웹캠을 동작 인식 카메라로 사용하기로 했고, 한 운동만 하는것이 아닌 여러개의 운동이 순서대로 진행되어야 했다.
웹캠을 활용하여 티처블머신에 적용하는것은 예전에 작성한 글에서도 한번 다뤘으므로 링크로 대체하도록 하겠다.
openVidu와 Teachable machine 사용해보기
우리 프로젝트는 실시간 화상 채팅을 하며 다같이 운동을 하도록 제공하는 서비스를 제공한다. 운동 동작(스쿼트, 런지 등등)을 카운트 해주어야 하는데, 이를 위해서 웹캠 영상을 통해 실시간
anji0.tistory.com
그 다음으로 어려웠던 점은 한 운동만 하는것이 아닌 여러 운동을 순서대로 진행되어야 하는것이다.
유저가 임의로 운동 순서를 설정하여 루틴을 만들어서 운동을 실행하면 그 순서대로 머신러닝 모델을 불러와서 영상을 분석해야 했다.
이 과정에서 한 동작마다 티처블머신 모델을 실행시키는 컴포넌트를 생성하고 해당 동작이 끝나면 컴포넌트를 삭제하는 방식으로 순서대로 운동을 진행시켰다.
위 컴포넌트 구조에서 운동을 시작하면 Start 컴포넌트를 마운트 시킨다.
이후 한 동작 마다 Timer와 TeachableMachine컴포넌트를 마운트 시키고 동작이 끝나면 다시 Timer와 TeachableMachine컴포넌트를 언마운트 시킨다. 각 동작의 성공 횟수를 카운트 하는 것은 TeachableMachine 컴포넌트 안에서 수행하고, 언마운트 될 때 배열에 기록한다.
예를 들어 스쿼트-> 버피 순으로 운동을 한다고 하면 다음 그림과 같은 순서로 마운트-언마운트 시킨다.
이러한 과정을 운동 루틴대로 반복한다.
보다시피 한 컴포넌트 내에서 여러 운동 모델의 개수를 측정할 수 있어야 한다. 따라서 중복 되는 로직을 최소화 시켜서 재사용가능한 함수를 만들도록 했다.
컴포넌트가 생성될 때 해당 운동에 맞는 모델을 전역 변수로 설정해 두고, 그 모델의 측정 api를 사용하여 횟수를 카운트한다.
//TeachableMachine.jsx
updateCount(data) {
data.forEach((res) => {
const doneAction = this.predicFunction(
res,
this.beforAction,
this.changeAction
)
if (doneAction) {
this.setState(
(state) => ({
successCount: state.successCount + 1,
}),
() => {
this.props.countSuccess()
}
)
}
})
}
//models.js
callbackSquat({ className, probability }, beforeAction, changeAction) {
// console.log("callbackSquat()")
// console.log(changeAction)
const action = className
const prob = probability.toFixed(2)
let correctDone = false
if (prob >= 0.85) {
if (action === "Squat") {
if (beforeAction === "Stand_Up") {
console.log("성공")
correctDone = true
}
}
changeAction(action)
}
return correctDone
}
동작별로 성공으로 판단할 확률과 로직이 달라야 하므로 동작별 성공 판단 로직 함수를 생성하고 성공 여부를 리턴한다.
TeachableMachine 컴포넌트는 이전 동작과 현재 동작을 변경할 함수를 위의 함수의 인자로 넘겨준다. 성공 로직 판단 함수 안에서 해당 인자들을 활용한다.
일급 객체인 js의 특성을 활용하여 코드를 작성할 수 있어서 재미있었던 과정이었다.
배운점
상태관리 라이브러리인 Redux를 활용하는 것은 물론, 다양한 state 변경에 따른 렌더링 변화를 올바르게 제어할 수 있는 방법을 익힐 수 있었다. 일례로 버튼을 클릭하면 바로 redux의 state를 변경하도록 dispatch 하는 로직을 짰는데 무한 렌더링이 일어났다. 이를 해결하기 위해서 버튼 클릭 여부를 담고있는 state를 하나 생성하고 이 state가 변경되면 원하는 동작을 수행하도록 변경하였더니 해결되었다. 이 과정에서 익힌 방법은 그 뒤의 여러 프로젝트에서도 유용하게 썼다.
또한 Teachable Machine 활용을 위해서 라이브러리 코드를 모두 열어보고 분석해 보는 경험을 처음 가져보았다. 복잡한 라이브러리가 아니긴 했으나 라이브러리를 뜯어보고 이를 활용해서 내가 필요한대로 코드를 작성할 수있는 경험을 해 볼 수 있었다. 이 과정에서 라이브러리란것이 생각보다 어려운것이 아니란것을 깨달았고 시간이 된다면 나도 라이브러리를 만들어보고싶다는 생각이 들었다.
리액트를 이전에도 써보긴 했으나 이렇게 많은 컴포넌트를 갖고 상태관리를 해 본 경험은 처음이었다. 이런 저런 시행착오를 겪기도 하고 리팩토링도 여러번 해 보면서 리액트를 조금 더 자유롭게 사용할 수 있게 되었다.
프로젝트를 하며 사용해본 redux는 생각보다 번거로운 과정이 있었고, 이를 경험삼아 다른 상태관리 라이브러리도 사용해보고싶단 생각도 많이 들었다. 이런 경험들이 쌓여서 계속해서 새로운 기술을 시도해보고싶은 의욕이 생기는듯하다. 이 프로젝트를 하기 전까지는 TypeScript의 필요성을 크게 느끼지 못했다. 오히려 코딩테스트용으로 쓰던 Java를 쓰며 철저하게 제한된 자료형이 불편하다고도 생각했는데 이 프로젝트를 하면서 정적 타입의 필요성을 크게 깨달았다. 코드를 아무리 뜯어봐도 잘못된 로직을 찾을 수 없었는데 숫자==문자열 이라고 써둔 코드에서 예기치못한 버그를 발생시키고 있었다. 이런 상황을 겪고나니 정적 타입의 필요성을 너무 크게 깨달았다.
또 협업을 하다보면 다른 사람의 코드도 많이 보게 되는데 타입스크립트가 아닌 js환경에서의 react는 인자로 받는 것이 모두 any로 표현되어 코드를 해석하는데 시간이 많이 걸리곤 했다. 이런 불편함들통해서 TypeScript의 필요성을 느꼈고 이 다음 프로젝트부터는 TypeScript를 적용하여 개발을 했다.
이 프로젝트를 통해서 React에 대한 능숙함을 키울 수 있었고, 다양한 기술을 써보고싶다는 욕심을 키워 나갈 수있었다. 개발을 많이 해 볼수록 더 많은 것을 배우고싶은 생각이드는것 같다. 이번 프로젝트에서 아쉬운점을 다음 프로젝트에서는 무조건 개선해가며 개발할 수있는 개발자가 되고싶다.