[React]React와 Supabase로 백분율 순위 표시

이보아·2024년 6월 19일

오늘은 IT 직군 테스트를 만들면서, Supabase 데이터베이스에서 직군 데이터를 가져와 상위 3개의 직군을 화면에 표시하고, 이를 백분율로 표현하는 방식을 구현했다.


1. Supabase 클라이언트 설정 및 데이터 가져오기⭐

  • 'JOB_POSITION' 테이블에서 데이터를 가져오기

유저가 설문을 한 후에 결과 값이 counter 컬럼에 결과 타입으로
누적된다. 저장된 타입별 counter 값을 사용하여 순위와, 백분율을 계산 한다.

Supabase 클라이언트를 설정한다. 이를 위해 .env.local 파일을 최상위 폴더에 생성한 후 Supabase URL과 API 키를 저장하고, createClient 함수를 사용해 클라이언트를 생성한다.

// src/supabase/supabaseClient
import { createClient } from '@supabase/supabase-js';

// env.local 파일에 있는 URL, KEY 값 가져오기 
const VITE_SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const VITE_SUPABASE_KEY = import.meta.env.VITE_SUPABASE_KEY;

const supabaseClient = createClient(VITE_SUPABASE_URL, VITE_SUPABASE_KEY);

export default supabaseClient;
  • 데이터 가져오기 함수

이제 'JOB_POSITION' 테이블에서 데이터를 가져오는 함수를 작성한다. 이 함수는 Supabase 클라이언트를 사용해 데이터를 쿼리하고, 결과를 반환하는 역활을 한다.

// src/supabase/api/jopPosition
import supabaseClient from '../supabaseClient';

export async function getJobPosition() {
  const { data: jobPosition, error } = await supabaseClient
    .from('JOB_POSITION')
  // JOB_POSITION 테이블에서 데이터를 가져옴
    .select('type, type_name, counter') 
  // type, type_name, counter 컬럼을 선택
    .order('counter', { ascending: false });
  // counter 값을 기준으로 내림차순으로 정렬
  
  if (error) {
    console.error('Error fetching job positions:', error);
    return [];
  }
  
  return jobPosition;
}

2. React 컴포넌트 작성⭐

  • 데이터 가져오기 및 상태 관리

React의 useEffectuseState를 사용해 컴포넌트가 렌더링될 때 Supabase에서 데이터를 가져오도록 설정한다. 데이터를 가져오는 동안 로딩 상태를 관리하기 위해 isLoading 상태를 추가했습니다.

// src/pages/ResultPage/ResultPage 
import { useEffect, useState } from 'react'; 
import { getJobPosition } from '../../supabase/api/jopPosition'; 
import ResultPageLayout from '../../components/ResultPage/ResultPageLayout'; 

export default function ResultPage() {
  const [jobPositions, setJobPositions] = useState([]); 
  const [isLoading, setIsLoading] = useState(true); // 로딩 상태 관리

  useEffect(() => {
    // 컴포넌트가 처음 렌더링될 때 데이터를 가져옵니다.
    async function fetchData() {
      // 비동기 함수로 데이터를 가져옵니다.
      const fetchedPositions = await getJobPosition(); // getJobPosition 함수를 호출하여 직군 데이터를 가져옵니다.
      setJobPositions(fetchedPositions); // 가져온 데이터를 상태 변수에 저장
      setIsLoading(false); // 데이터를 모두 가져왔으므로 로딩 상태를 false로 설정한다.
    }

    fetchData(); // fetchData 함수를 호출하여 데이터를 가져옵니다.
  }, []); 
  // 빈 배열을 두 번째 인수로 전달하여 컴포넌트가 처음 렌더링될 때만 useEffect가 실행되도록 합니다. 

  // 결과 페이지 레이아웃 컴포넌트를 렌더링하고, 직군 데이터와 로딩 상태를 props로 전달합니다
  return <ResultPageLayout jobPositions={jobPositions} isLoading={isLoading} />;
}

3. 결과 페이지 레이아웃 생성

데이터를 화면에 표시하기 위해 'ResultPageLayout' 컴포넌트에 'jobPositions'와 'isLoading'을 'props'로 받아 상위 3개의 직군을 보여줍니다. 만약 데이터가 로딩 중이라면 'Skeleton'컴포넌트를 사용해 로딩 상태를 보여줍니다.

// src/components/ResultPage//ResultPageLayout
import PercentageTable from './PercentageTable';
function ResultPageLayout({ jobPositions, isLoading }) {
  return (
    <div>
      <PercentageTable jobPositions={jobPositions} isLoading={isLoading} />
    </div>
  );
}
export default ResultPageLayout;

4 .PercentageTable 컴포넌트 생성⭐⭐

PercentageTable 컴포넌트는 counter의 데이터를 순위별로 정렬하고, 상위 3개의 직군을 백분율로 계산하여 화면에 표시합니다.

4.1 컴포넌트 상태 관리

  • showMore는 버튼이 클릭되었는지 여부를 저장합니다.
function PercentageTable({ jobPositions, isLoading }) {
  // '더보기' 버튼 상태를 관리합니다.
  const [showMore, setShowMore] = useState({});

4.2 '더보기' 버튼 토글 함수

  • toggleShowMore 함수는 '더보기' 버튼이 클릭되었을 때 호출됩니다.
    rank(현재 순위)이며, showMore 상태를 업데이트하여 현재 순위에 대한 상태를 반전시킨다.

  • 동일한 순위에 직군이 많을시 생성 됩니다. (영역이 좁기때문에 토글 버튼으로 관리)

  // '더보기' 버튼을 클릭했을 때 상태를 변경합니다.
  const toggleShowMore = (rank) => {
    setShowMore((prevState) => ({
      ...prevState,
      [rank]: !prevState[rank],
    }));
  };

4.3 직군의 총합 계산

  • reduce 함수는 모든 직군의 counter 값을 더하여 총합을 계산한다.sum은 누적 값이며, item.counter는 현재 항목의 값이다.
  // 모든 직군의 총합을 계산
  const total = jobPositions.reduce((sum, item) => sum + item.counter, 0);

4.4 백분율 계산 함수

  • calculatePercentage 함수는 주어진 값을 총합으로 나눈 후 백분율로 변환합니다.total이 0이 아닐 때만 계산하며, 그렇지 않으면 0을 반환합니다.
  // 각 직군의 비율을 계산합니다.
  const calculatePercentage = (value) => {
    return total ? Math.round((value / total) * 100) : 0;
  };

4.5 직군 정렬

rankedPositionsjobPositions를 순위별로 정렬한 배열입니다.
reduce 함수를 사용하여 각 직군에 순위를 부여합니다.

-reduce 함수는 배열을 순회하면서 누적값(acc)을 계산합니다.
여기서는 acc가 정렬된 직군을 담을 배열입니다.

  • acc: 누적값, 여기서는 정렬된 직군을 담을 배열
    item: 현재 배열 요소, 여기서는 직군 데이터
    index: 현재 배열 요소의 인덱스
    array: 원본 배열, 여기서는 jobPositions

  • const prev = array[index - 1]; -> prev는 이전 요소를 의미함 index - 1을 사용하여 현재 요소의 이전 요소를 가져옵니다.
    첫 번째 요소일 경우, prev는 undefined가 됩니다.

  • item.rank = prev && item.counter === prev.counter ? prev.rank : index + 1;

prev가 존재하고(prev &&), 현재 요소와 이전 요소의 counter 값이 같으면(item.counter === prev.counter), 이전 요소의 순위를 현재 요소에 할당합니다.
그렇지 않으면, 현재 요소의 순위를 index + 1로 설정합니다. index는 0부터 시작하므로, 순위는 index + 1로 설정됩니다.

  • acc.push(item); -> 현재 요소 (item)를 누적값 배열 (acc)추가
  // 직군을 순서대로 정렬합니다.
  const rankedPositions = jobPositions.reduce((acc, item, index, array) => {
    const prev = array[index - 1];
    item.rank = prev && item.counter === prev.counter ? prev.rank : index + 1;
    acc.push(item);
    return acc;
  }, []);

4.6 직군 그룹화 (직군이 여러개 나왔을때)

  • groupedPositions는 같은 순위의 직군들을 그룹화한 객체입니다.
    reduce 함수를 사용하여 각 순위별로 직군들을 배열에 저장합니다.
  // 같은 순위의 직군들을 그룹화합니다.
  const groupedPositions = rankedPositions.reduce((acc, item) => {
    acc[item.rank] = acc[item.rank] ? [...acc[item.rank], item] : [item];
    return acc;
  }, {});

4.7 상위 3개의 순위 선택

  • topJobPositions는 상위 3개의 순위를 저장한 배열입니다.
    Object.keys 함수를 사용하여 순위들을 배열로 변환하고, slice 함수를 사용하여 상위 3개만 선택한다.
  // 상위 3개의 순위만 선택합니다.
  const topJobPositions = Object.keys(groupedPositions).slice(0, 3);

4.8 백분율 계산

  • percentages는 각 순위의 백분율을 저장한 배열입니다.
    map 함수를 사용하여 각 순위의 백분율을 계산합니다.
 // 각 순위의 백분율을 계산합니다.
  const percentages = topJobPositions.map((rank) => {
    return calculatePercentage(groupedPositions[rank][0].counter);
  });
  • 전체 코드
// src/components/ResultPage/PercentageTable.jsx
import styled from 'styled-components';
import SkeletonBar from '../../assets/styles/skeleton/Bar';
import SkeletonCircle from '../../assets/styles/skeleton/Circle';
import { useState } from 'react';

// 컴포넌트 스타일 생략 

function PercentageTable({ jobPositions, isLoading }) {
  const [showMore, setShowMore] = useState({}); // 더보기 버튼 상태 관리

  // 더보기 버튼을 클릭했을 때 상태를 토글하는 함수
  const toggleShowMore = (rank) => {
    setShowMore((prevState) => ({
      ...prevState,
      [rank]: !prevState[rank],
    }));
  };

  // 직군별 합계를 계산하는 함수
  const total = jobPositions.reduce((sum, item) => sum + item.counter, 0);

  // 비율을 계산하는 함수
  const calculatePercentage = (value) => {
    return total ? Math.round((value / total) * 100) : 0;
  };

   // 직군을 순위별로 정렬하는 함수
  const rankedPositions = jobPositions.reduce((acc, item, index, array) => {
    const prev = array[index - 1];
    item.rank = prev && item.counter === prev.counter ? prev.rank : index + 1;
    acc.push(item);
    return acc;
  }, []);

  // 같은 순위의 직군들을 그룹화하는 함수
  const groupedPositions = rankedPositions.reduce((acc, item) => {
    acc[item.rank] = acc[item.rank] ? [...acc[item.rank], item] : [item];
    return acc;
  }, {});

  // 상위 3개의 순위만 선택
  const topJobPositions = Object.keys(groupedPositions).slice(0, 3);

  // 각 순위별 백분율을 계산
  const percentages = topJobPositions.map((rank) => {
    return calculatePercentage(groupedPositions[rank][0].counter);
  });

마무리 😶‍🌫️

이번 프로젝트에서 중복된 직군을 그룹화하고 상위 3개의 직군을 백분율로 정확하게 계산하는 부분이 가장 어려웠다. 앞으로도 다양한 데이터셋을 다루면서 더 효율적이고 중복된 데이터 처리 방식에 대해서도 생각해 봐야겠다!

profile
매일매일 틀깨기

0개의 댓글