4 분 소요

Introduction

코딩을 할 때 데이터를 다루다 보면 데이터의 정렬이 필요할 때가 있다. 백엔드와 프론트엔드 모두 이 정렬 기능이 필요한 경우가 많은데 일반적으로는 다음과 같은 장점이 있다.

  • 정보 접근성 향상
    • 빠르게 원하는 정보 찾기
    • 효율적인 탐색
  • 정보의 가독성 및 이해도 증가
    • 데이터 구조화
    • 의미있는 비교 가능 (가격순, 크기순, 시간순)
  • 분석 및 의사 결정 지원
    • 트렌드 파악
    • 최대/최소값 찾기
  • 사용자 경험 향상
    • 선호도에 맞춘 정렬
    • 편리한 필터링과 조합 가능

정렬은 단순히 데이터를 나열하는 것 이상의 가치를 지니며, 탐색, 비교, 분석, 관리의 효율성을 극대화 해준다. 사람들이 정렬 기능을 사용하는 이유는 결국 더 나은 결정을 내리고, 원하는 정보를 빠르게 찾으며, 데이터를 더 명확하게 이해하고자 하는데 있다.

그렇다면 이제 백엔드와 프론트엔드에서 정렬기능을 어떻게 구현되어 사용되는지 알아보겠다.

Sort() in Backend

백엔드에서도 정렬 기능이 필요한 경우가 많다. 주로 대규모 데이터 처리와 효율적인 데이터 제공을 위해 백엔드에서 데이터를 정렬해 클라이언트에 전달하는 것이 중요하다. 백엔드의 주요 역할 중 하나는 데이터를 데이터베이스에서 클라이언트의 요청에 맞게 추출해서 다시 클라이언트로 보내주는 경우인데, 이때 정렬 기능이 사용될 수 있다. 이렇게 되면 클라이언트에서 추가로 정렬을 할 필요가 없어서 효율적이다. 이때는 보통 데이터베이스의 내장 정렬기능을 활용하게 되는데, 이를 활용하면 백엔드에서 데이터를 더 빠르고 일관되게 정렬할 수 있다. 다음은 SQLNOSQL 데이터베이스에서 정렬하는 예제이다.

  1. SQL 데이터베이스에서 created_at 필드를 기준으로 정렬
        SELECT * FROM orders ORDER BY created_at DESC;
    
  2. MongoDB (NOSQL)에서 페이징과 함께 정렬 백엔드에서는 종종 페이징과 정렬이 결합된다. 페이징은 전체 데이터를 페이지 단위로 나누어 제공하는 방식이며, 이때 정렬을 추가하면 사용자가 원하는 순서대로 데이터를 조회할 수 있다.
        db.collection('orders')
        .find()
        .sort({ createdAt: -1 }) // 최근 순으로 정렬
        .skip((page - 1) * limit) // 페이지 건너뛰기
        .limit(limit); // 페이지당 데이터 수
    
  3. 비즈니스 로직을 위한 정렬 백엔드에서 특정 비즈니스 로직을 수행하기 위해 데이터가 특정 순서로 필요할 수 있다. 예를 들어, 주문 처리 시스템에서 주문을 생성 순으로 정렬하여 처리하거나, 이벤트가 발생한 시간순으로 로그를 정렬하는 경우가 있다.
    const orders = await Order.find({ status: 'pending' }).sort({ createdAt: 1 });
    processOrders(orders); // 오래된 주문부터 처리

요약하자면 백엔드에서 정렬기능을 사용하는 이유는 다음과 같다.

  • 성능 향상: 대규모 데이터를 백엔드에서 미리 정렬해 클라이언트로 전송하면 성능이 개선된다.
  • 일관성: 백엔드에서 정렬 기준을 API에서 제공하면 클라이언트에 일관된 데이터를 제공한다.
  • 비즈니스 로직 지원: 특정 작업에 필요한 순서대로 데이터를 정렬하여 처리할 수 있다.

Sort() in Frontend

나는 현재 프론트엔드 개발자로 프로젝트에 참여하고 있고, 사이드 프로젝트로도 프론트엔드 작업이 많아 프론트엔드에서 정렬을 구현해야 하는 User Story 들을 많이 경험했다.

  • 표 (table) 에서 열 (column)을 클릭했을 때 클릭한 열의 값들이 오름 또는 내림차순으로 정렬하기
  • 데이터 리스트를 이름, 날짜 그리고 부분 (Percentage) 별로 오름 또는 내림차순으로 정렬하기

위의 두가지 경우가 대표적인 User Story 였다.

이 경우들은 데이터를 백엔드에서 정렬을 해서 받는 것이 큰 의미가 없었다. 왜냐하면 여기에 필요한 정렬 기능은 말 그대로 “사용자 정의” 정렬 기능이기 때문이었다. 따라서 백엔드에서 받은 데이터를 사용자의 요청에 따라 프론트엔드에서 정렬을 구현해야만 했다.

sort() in javascript

Javascriptsort() 메서드는 배열의 요소를 정렬하여 기존 배열을 수정하며, 정렬된 배열을 반환한다. 즉 배열 을 반환한다. 숫자, 날짜, 사용자 정의 객체 등을 올바르게 정렬하려면 compareFunction을 사용해야 한다.

기본 문법

  array.sort([compareFunction])

여기서 compareFunction은 배열의 각 두 요소를 비교하여 정렬 순서를 결정하는 함수이다.

compareFunction(a, b)는 두 요소 a, b를 받아 다음과 같이 동작한다: (a, b) => a - b.

  • 반환 값이 0 보다 작은 경우: ab보다 앞에 있어야 한다 (오름차순).
  • 반환 값이 0 보다 큰 경우: ab보다 뒤에 있어야 한다 (내림차순).
  • 반환 값이 0 인 경우: ab의 순서를 바꾸지 않는다.

이제 이 compareFunction(a, b) 를 정렬 기준에 맞게 잘 써주면 된다.

숫자 정렬

숫자를 오름차순과 내림차순으로 정렬하는 건 어렵지 않다. 위의 compareFunction(a, b) 기본 형태인 (a, b) => a - b 를 그대로 사용해주면 된다.

  const numbers = [40, 100, 1, 5, 25, 10];

  // 오름차순
  numbers.sort((a, b) => a - b);
  console.log(numbers); // [1, 5, 10, 25, 40, 100]

  // 내림차순
  numbers.sort((a, b) => b - a);
  console.log(numbers); // [100, 40, 25, 10, 5, 1]

문자열 (string) 정렬

문자열을 오름차순과 내림차순으로 정렬하려면 localeCompare() 메서드를 사용하는 것이 좋다. 이 메서드는 문자열을 알파벳순으로 정확하게 비교하며, 다양한 언어의 정렬을 지원한다. 그래서 이 경우에는 compareFunction(a, b) 의 형태가 약간 바뀐다.

  const words = ["banana", "apple", "orange", "mango"];

  // 오름차순 (알파벳순)
  words.sort((a, b) => a.localeCompare(b));
  console.log(words); // ["apple", "banana", "mango", "orange"]

  // 내림차순 (알파벳순의 반대)
  words.sort((a, b) => b.localeCompare(a));
  console.log(words); // ["orange", "mango", "banana", "apple"]

여기서 a.localeCompare(b)ab 보다 알파벳 순으로 앞에 있으면 음수를 반환하여 오름차순이 되도록 하고, 반대로 정렬하려면 b.localeCompare(a)을 사용하면 된다.

날짜 정렬

날짜를 정렬할 때는 Date 객체를 사용하는 것이 일반적이다. 두 날짜 객체를 getTime() 메서드를 사용해 밀리초 (ms) 로 변환해 비교할 수도 있다.

  const dates = [
    new Date('2023-12-01'),
    new Date('2021-01-05'),
    new Date('2022-06-15')
  ];

  // 오름차순 (과거에서 미래 순서로)
  dates.sort((a, b) => a.getTime() - b.getTime());
  console.log(dates); // [2021-01-05, 2022-06-15, 2023-12-01]

  // 내림차순 (미래에서 과거 순서로)
  dates.sort((a, b) => b.getTime() - a.getTime());
  console.log(dates); // [2023-12-01, 2022-06-15, 2021-01-05]

여기서 a.getTime() - b.getTime()ab의 날짜 차이를 반환하여 오름차순 정렬이 되도록 하고 내림차순을 원할 때는 b.getTime() - a.getTime()을 사용한다.

사용자 정의 객체의 특정 속성으로 정렬

이런 Task가 있었다. 사용자 객체의 두개의 속성의 비율을 계산해, 그 결과 값으로 정렬을 해야 하는 것이 과제였다. 이 경우는 결국에는 숫자 정렬 과 똑같았지만, Edge Case를 고려해줘야 했다. 비율을 계산한다는 것은 하나의 수를 다른 하나의 수로 나눈 다는 뜻이었지만, 나눠지는 속성이 0 이거나 undefined일 경우는 NaN값이 나와 오류가 생기게 된다.

다음은 내가 구현한 코드의 일부분이다.

  if (sortBy === "ratio") {
      sortedData = data?.sort((a, b) => {
        const a_ratio = a.dominator ? (a.numerator || 0) / a.dominator : 0;
        const b_ratio = b.dominator ? (b.numerator || 0) / b.dominator : 0;

        if (sortDirection) {
          return b_ratio - a_ratio;
        } else {
          return a_ratio - b_ratio;
        }
      });
    }

이 코드는 sortingData() 라는 사용자 정의 함수로, 정렬 기준 (sortBy) 와 정렬 방향 (sortDirection)을 클라이언트에서 받아서 정렬을 하는 함수이다. a_ratio, b_ratio는 각각 dominatornumerator의 비율로 계산이 되어지는데, 이때 dominator 값이 있으면 그 값으로 나눠주고, 아니면 0 으로 정의를 했다. 그리고 numerator의 값도 있으면 그 값을 쓰고, 아니면 0 으로 계산이 되게 했다.

요약을 하자면,

  • 숫자 정렬: sort((a, b) => a - b);
  • 문자열 정렬: sort((a, b) => a.localeCompare(b));
  • 날짜 정렬: sort((a, b) => new Date(a) - new Date(b));

이렇게 요약을 할 수 있겠다.

댓글남기기