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

유저가 설문을 한 후에 결과 값이 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;
}
React의
useEffect와useState를 사용해 컴포넌트가 렌더링될 때 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} />;
}
데이터를 화면에 표시하기 위해 '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;
PercentageTable컴포넌트는 counter의 데이터를 순위별로 정렬하고, 상위 3개의 직군을 백분율로 계산하여 화면에 표시합니다.
function PercentageTable({ jobPositions, isLoading }) {
// '더보기' 버튼 상태를 관리합니다.
const [showMore, setShowMore] = useState({});
toggleShowMore 함수는 '더보기' 버튼이 클릭되었을 때 호출됩니다.
rank(현재 순위)이며, showMore 상태를 업데이트하여 현재 순위에 대한 상태를 반전시킨다.
동일한 순위에 직군이 많을시 생성 됩니다. (영역이 좁기때문에 토글 버튼으로 관리)
// '더보기' 버튼을 클릭했을 때 상태를 변경합니다.
const toggleShowMore = (rank) => {
setShowMore((prevState) => ({
...prevState,
[rank]: !prevState[rank],
}));
};
reduce 함수는 모든 직군의 counter 값을 더하여 총합을 계산한다.sum은 누적 값이며, item.counter는 현재 항목의 값이다. // 모든 직군의 총합을 계산
const total = jobPositions.reduce((sum, item) => sum + item.counter, 0);
calculatePercentage 함수는 주어진 값을 총합으로 나눈 후 백분율로 변환합니다.total이 0이 아닐 때만 계산하며, 그렇지 않으면 0을 반환합니다. // 각 직군의 비율을 계산합니다.
const calculatePercentage = (value) => {
return total ? Math.round((value / total) * 100) : 0;
};
rankedPositions는jobPositions를 순위별로 정렬한 배열입니다.
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로 설정됩니다.
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;
}, []);
groupedPositions는 같은 순위의 직군들을 그룹화한 객체입니다.reduce 함수를 사용하여 각 순위별로 직군들을 배열에 저장합니다. // 같은 순위의 직군들을 그룹화합니다.
const groupedPositions = rankedPositions.reduce((acc, item) => {
acc[item.rank] = acc[item.rank] ? [...acc[item.rank], item] : [item];
return acc;
}, {});
topJobPositions는 상위 3개의 순위를 저장한 배열입니다.Object.keys 함수를 사용하여 순위들을 배열로 변환하고, slice 함수를 사용하여 상위 3개만 선택한다. // 상위 3개의 순위만 선택합니다.
const topJobPositions = Object.keys(groupedPositions).slice(0, 3);
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개의 직군을 백분율로 정확하게 계산하는 부분이 가장 어려웠다. 앞으로도 다양한 데이터셋을 다루면서 더 효율적이고 중복된 데이터 처리 방식에 대해서도 생각해 봐야겠다!