본문 바로가기

코딩/트러블슈팅

크로스핏의 다양한 기록 정렬: 17:17과 3R+12, 누가 더 잘한 걸까?

크로스핏 박스 전용 웹 사이트 'wodLog'를 개발하면서 가장 흥미로웠던 것은 '다양한 기록 측정 방식에 따른 순위 정렬 구현'이었습니다. 이 글에서는 복잡한 정렬 요구사항을 어떻게 해결했는지, 그 과정에서 발생한 문제들과 최적화 과정을 공유하고자 합니다.

 

우선 크로스핏에 대해서 아주 간략히 말씀드리자면, 크로스핏에서 경쟁은 빼놓을 수 없는 요소이며, 이는 기록을 통해 순위가 결정됩니다.

 

프로젝트 기획 전, 10개의 크로스핏 웹 사이트를 조사해 보니 8개가 네이버 카페로 운영되고 있는 것을 확인하였습니다. 이는 크로스핏 전용 사이트가 아니다 보니 여러 불편함이 있었습니다.

  • 기록이 단순 사진으로만 등록되어 순위 파악이 어려움
  • 사진이 잘려서 올라가는 경우가 존재
  • 필체가 알아보기 힘든 경우가 존재

이런 문제들을 해결하고자 '기록 사진을 찍어 웹 사이트에 올리면 순위가 자동으로 계산되어 정렬되는 기능'을 구상하게 되었습니다.

 

🔍 문제 상황 : 3가지 측정 방식의 공존

 

위 사진에서 볼 수 있듯이, 크로스핏의 기록 측정 방식은 3가지로 혼재되어 있습니다.

  • 시간 (예: `17:17`)
  • 라운드 (예: `3R+12`)
  • 개수 (예: `200` 사진에는 존재하지 않지만 개수만 적는 경우도 있음)

 

측정 방식별로 우선순위와 기준이 달라 아래와 같은 정렬 기준이 필요했습니다.

참고로, 크로스핏은 정해진 운동을 시간 내에 수행했다면 시간을 적고, 수행하지 못했을 경우 수행한 라운드 수를 적게 됩니다.

1. 우선순위
- 시간 > 라운드 > 개수

2. 정렬 순서
- 시간 : 짧은 순으로 정렬
- 라운드/개수 : 많은 순으로 정렬

예: `200`, `3R+12`, `3R`, `17:17` 기록은
   `17:17`, `3R+12`, `3R`, `200` 순으로 정렬 필요

 

 

💡 문제 해결 과정

🔧 첫 번째 시도 : 소수점 변환 방식

기준을 잡은 후, 가장 먼저 고민했던 것은 라운드 표기 방식이었습니다. 예를 들어 `3R + 12`와 같은 기록을 어떻게 정렬할 것인가?

처음에는 '`R`을 마침표 `.`로 바꾸면 어떨까?'라고 단순하게 생각했습니다.

// 초기 구현 코드

// 이미지 기록을 업로드 후 텍스트로 변환한 결과에서 추출하는 과정
// 텍스트로 변환된 문자열을 정규식으로 이름, 기록 각각 추출
  const handleExtract = (result: string, type: string) => {
    const regExp = /(\p{L}+)\s+(\d+)(?:\s*R\s*\+\s*(\d+))?|\p{L}+\s+\d+/gu;
    const extractedRecords = [];
    let match;
    // regExp.exec(): 정규 표현식에 해당하는 문자열을 검색. 패턴이 존재하면 문자열의 배열 반환, 일치하는 패턴이 없으면 null반환
    while ((match = regExp.exec(result)) !== null) {
      const name = match[1];
      let record: number;

      // 이름 뒤에 숫자만 있는 경우, `R`과 함께 record를 만듬
      if (!match[3]) {
        record = parseFloat(match[2]);
      } else {
        // DB 저장될 때는 `R`를 `.`으로 바꿔서 소수로 저장
        record = parseFloat(`${match[2]}.${match[3]}`);
      }
    // ... 추가 처리 로직
  }
};
// 화면 출력 시 다시 원래 형식으로 변환(`.` -> `R`)
{record && !Number.isInteger(record)
                ? record.toString().replace(".", "R + ")
                : record}

 

이 방식으로 구현하니 `3R + 12`는 `3.12`가 되어 문자열 정렬로도 원하는 결과를 얻을 수 있었습니다. 하지만 자바스크립트에서는 부동소수점을 사용하기에 연산 결과가 정확하지 않을 수 있는 잠재적인 위험성을 발견하게 되었습니다. 현재는 단순 문자열 변환만 하고 있지만, 추후 연산이 필요할 경우에는 문제가 발생할 수도 있다는 생각이 들게 되어 소수점 변환 방식이 아닌 다른 방법을 고민하게 되었습니다.

 

 

부동소수점 연산의 부정확성 예시

console.log(0.1 + 0.2); // 0.30000000000000004 (오차 발생)

 

 

🔧 두 번째 시도 : 정수 기반 변환

결론적으로 모든 값을 정수로 변환하는 방식을 선택하게 되었습니다.

- 라운드: 라운드의 `R`을 `1000`으로 변환하여 계산 (3R+12 → 3 * 1000 + 12 → 3012)
- 시간: 시간의 `:`을 `60`으로 변환하여, 즉 분을 초 단위로 계산 (17:17 → 17 * 60 + 17 → 1037)

 

이렇게 하면 모든 값이 정수로 변환되어 비교가 명확해지고 데이터의 일관성도 생기게 됩니다.

이후 기록을 등록하면 원하던 대로 시간 > 라운드 > 개수 순으로 기록들이 정렬되는 모습을 볼 수 있었습니다.

 

🔧 성능 개선 : 실시간 계산 방식 → 사전 계산 방식

개발을 완료하고 테스트를 하던 중, 200개 정도의 기록을 한 번에 정렬하는데 10초나 걸리는 문제를 발견했습니다.

원인을 분석해 보니 아래와 같은 문제가 있었습니다.

  1. 페이지 접속할 때마다 모든 기록을 다시 계산
  2. 정렬된 결과를 저장하지 않아서 매번 재계산 필요

이 페이지를 만들 때 단순히 다양한 기록 측정 방식에 따른 순위 정렬을 어떻게 해야 할지에만 초점을 맞추다 보니 이러한 정렬 시점, 정렬값 계산 방법에 대해서는 생각하지 못해 발생한 문제였습니다. 이후 정렬 시점(페이지 접속 시 vs 기록 등록 시)과 정렬값 계산 방법(실시간 계산 vs 사전 계산)을 고민하게 되었으며, 최종 해결한 방법은 기록 등록 시 정렬값 사전 계산하는 방법을 선택하여 코드를 수정하게 되었습니다.

 

DB에 `sortableRecord` 컬럼을 추가하여 기록이 등록될 때 한 번만 계산하고 그 값을 저장하게 하였습니다.

 

이 개선으로 200개의 기록 처리 시간이 10초에서 6초로 40% 단축할 수 있었고 페이지 재접속 시에도 즉시 정렬된 결과를 확인할 수 있었습니다.


 

 

✏️ 개선 결과

  • 200개의 기록 처리 시간 40% 단축(10초 → 6초)
  • 데이터 정확성 및 일관성 개선
  • 페이지 재접속 시에도 즉시 정렬된 결과 확인 가능

🔍 남겨진 문제 : 광학 문자 인식 OCR

번외로 이미지 글씨를 인식하는 코드는 라이브러리를 사용했는데, 오픈소스 라이브러리 중에서 그나마 인식률이 좋은 테서렉서(Tesseract.js)를 선택했습니다. 하지만 실제 테스트 결과 몇 가지 한계점을 발견했습니다.

  1. 손 글씨 텍스트 변환 성공률이 낮음
  2. 글자 크기와 모양에 따라 텍스트 변환 성공률 편차가 큼
  3. 이미지 품질에 매우 민감함

원래 의도라면 회원들의 손 글씨 기록을 자동으로 인식하려고 하였으나 텍스트 변환 성공률이 좋지 못하였습니다.

아래는 제일 손글씨 같은 폰트를 이미지로 등록하여 텍스트로 변환한 결과입니다.

 

결국 변환 실패한 경우를 별도로 분류하여 아래 순서로 정렬하게 끔, 최종 수정하였습니다.

시간 > 라운드 > 개수 > 기록 추출 시도 실패

 

 

 

기록 변환을 실패하면 `기록 추출 시도 실패: 인식된 내용`으로 처리되는데, 시간이 가능해지면 사용자가 각 기록을 직접 수정할 수 있는 기능을 추가할 계획입니다.

 

🌟 마치며

이 프로젝트를 통해 배운 것은 겉보기에 간단해 보이는 문제도 실제로 구현하다 보면 여러 문제가 숨어있다는 점입니다. 개발을 하면 할수록 새로운 문제를 마주치며 하나씩 해결하는데 재미있었습니다. 또한 부동소수점 처리와 같이 현재는 문제없이 동작하더라도 미래의 확장성과 유지보수를 고려해야 한다는 점, OCR 인식 실패 처리와 같이 해결책이 없을 때는 현실적인 차선책을 찾는 것도 중요하다는 점 등을 깨닫게 되었습니다.