리액트 첫 개인과제 - Olympic Medal Tracker 만들기 🥇🥈🥉

조아라·2024년 10월 30일
2
post-thumbnail

리액트 첫 주 개인과제를 받았다.

이번 프로젝트의 목표는

  • 리액트 컴포넌트와 훅(useState)을 다룰 수 있어요.
  • 리액트에서 이벤트를 관리할 수 있어요.
  • 리액트의 state, props 를 확실히 이해하고 사용할 수 있어요.

목표는 이랬고 내가 구현해야 할 기능들이 몇가지 있었는데
로드맵으로 살펴보자 😊

Day 1: 기본 레이아웃 설정 및 입력 폼 구현

  • 1. 프로젝트 셋업

  • Vite로 리액트 프로젝트를 셋업하고, 리액트 어플리케이션을 한번 켜보세요!
  • Vite 셋업에 어려움이 있다면 공식 문서를 참고하세요: Vite 공식문서.

먼저 프로젝트를 Vite를 이용해서 만들어야 하기 때문에 터미널에서

npm create vite@latest

입력해서 만들어주고 프로젝트명을 나는 react-personal01로 줬다.
그러면 리액트로 만들건지 아니면 다른 프레임워크를 사용할건지 목록이 뜨는데
거기서 리액트를 선택 ❗

그 뒤에 언어는 자바스크립트 선택 ❗

  • 기본 레이아웃 구성

  • App.jsx에 기본 레이아웃을 설정하여 UI의 뼈대를 만듭니다.
  • 정렬을 좀 연습해 볼게요. 어플리케이션이 항상 가로 기준으로 화면 중앙에 배치되게 합니다.
  • 이후, 백그라운드 컬러, 상단 여백 등등 기본적인 레이아웃을 더 넣어줍니다..

UI 뼈대를 만든다. 이게 자바스크립트랑 가장 다른 점이었는데,
리액트는 컴포넌트 개념이 있기 때문에 App.jsx에 컴포넌트를 입력해주는 방식이다.
내가 만든 App.jsx는 일단 이렇다

import React, { useState } from 'react';
import './App.css'
import './components/Header.css'
import './components/MedalForm.css'
import './components/Footer.css'
import './components/MedalList.css'
import Header from "./components/Header";
import MedalForm from "./components/MedalForm";
import MedalList from "./components/MedalList";
import Footer from "./components/Footer";

const App = () => {
  const [countries, setCountries] = useState([]);

  const handleDelete = (index) => {
    setCountries(countries.filter((_, i) => i !== index));
  };

  return (
    <>
      <div className="app-container">
        <Header />
        <div className="medal-form">
          <MedalForm countries={countries} setCountries={setCountries} />
        </div>
        <MedalList countries={countries} onDelete={handleDelete} />
      </div>
      <div>
        <Footer />
      </div>
    </>
  );
};

export default App;

난 최대한 컴포넌트를 잘게 쪼개서 App.jsx에는 많은 함수들이 없기를 바랬어서 이렇게 간단하게 적어줬다 😯

  • 입력 폼 UI 구현

  • 나라 이름, 금메달, 은메달, 동메달 입력 필드를 추가하고 제출 버튼을 추가하여 폼을 완성합니다.
  • 제출 버튼 클릭 시 데이터를 추가할 수 있도록 onSubmit 이벤트 핸들러를 설정하고, 기본 상태를 관리할 useState 훅을 사용해 초기 상태를 설정합니다.
  • 제출 후 입력 필드를 초기화하도록 처리합니다.
  • onSubmit 을 사용하기 위해서 Input 들은 Form 안에 위치되어야 함을 잊지 말아주세요.

이 부분은 내가 input을 리액트로 막 공부하고 나서 만든 부분이라 완전 금방 만들었다(ㅎㅎ)
내 코드들을 조금 쪼개서 보자면 먼저 통합 이벤트와 객체 방식으로 state를 모아둔 것❗

import { useState } from "react";

//입력 받아야 할 것
//1.국가명
//2.금은동 메달 개수

const MedalInputForm = ({ countries, setCountries }) => {
  const [input, setInput] = useState({
    country: "",
    gold: "",
    silver: "",
    bronze: "",
  });

  const onChange = (e) => {
    const { name, value } = e.target;
    if (name === "country") {
      setInput({
        ...input,
        [name]: value,
      });
    } else {
      const numValue = parseInt(value);
      if (value === "" || (numValue >= 0 && numValue <= 99)) {
        setInput({
          ...input,
          [name]: value,
        });
      }
    }
  };

가장 먼저 useState를 사용해주기 위해 import해주고,
바로 아래 내가 입력 받아야 할 목록을 주석으로 조금 정리해 두었다.

난 나라들과 금메달 은메달 동메달의 개수를 input 받아야 하니까 저렇게 적어주고,
아래 onChange는 input받은 내용들을 name과 value로 이벤트를 주기로 했다.

일단 조건문을 살펴보면
➡️ if (name === "country") name값이 "country"라면
➡️ setInput 에서 나머지 input값들 (...input)은 두고 네임값만 변하도록 뒀고
➡️ else { const numValue = parseInt(value); if (value === "" || (numValue >= 0 && numValue <= 99))
여기에서는 뒤에 나오는 조건이 메달의 개수가 100을 넘지않는 두자리수 정수여야 한다를 맞추기 위해 넣어줬다. 일단 빈 문자열 또는 numValue가 0이상 99랑 같거나 작다로 줬고,
(빈 배열을 처음에 넣지않으니까 숫자가 지워지지 않았다 -> catch❗)
➡️ 나머지값은 변동하지않게 해주었다.

다음은 input되는 부분인데,

return (
  <form action="submit" className="MedalInputForm">
    <div>
      <p>국가명</p>
      <input 
      name="country" 
      value={input.country} 
      onChange={onChange} 
      />
    </div>

    <div>
      <p>금메달</p>
      <input
        name="gold"
        type="number"
        value={input.gold}
        onChange={onChange}
        placeholder="0~99까지 정수 입력"
      />
    </div>

    <div>
      <p>은메달</p>
      <input
        name="silver"
        type="number"
        value={input.silver}
        onChange={onChange}
        placeholder="0~99까지 정수 입력"
      />
    </div>

    <div>
      <p>동메달</p>
      <input
        name="bronze"
        type="number"
        value={input.bronze}
        onChange={onChange}
        placeholder="0~99까지 정수 입력"
      />
    </div>

    <div className="btn-container">
      <button className="buttons" onClick={onAddCountry}>
        국가 추가
      </button>

      <button className="buttons" onClick={onUpdate}>
        업데이트
      </button>
    </div>
  </form>
);
};

export default MedalInputForm;

위에서부터 아래로 나라 메달 추가버튼과 업데이트 버튼으로 나열되어 있다.
value값과 name값도 맞춰서 넣어줬고, 숫자를 적는 부분에는 저렇게 명시까지 해주었다.
사용하는 onChange도 각자 다 맞춰 넣어줬다.


Day 2: 메달 집계 리스트 출력 및 CRUD 기능 구현

  • CRUD 기능 - Create 및 Read

  • Create 기능 구현:
    • 제출 버튼 클릭 시 새로운 국가와 메달 정보를 리스트에 추가하고 화면에 표시됩니다.
  • Read 기능 구현:
    • 리스트에 표시된 국가별 메달 집계를 확인할 수 있도록 UI에 반영합니다.

여기에서 가장 중요한 button 😣
여기가 아무래도 함수를 넣어줘야 하다보니까 등장한다. state와 props ❗❗❗

   /* 국가 추가 누르면 나라와 메달 개수 추가 함수 */
  }
  const onAddCountry = (e) => {
    e.preventDefault();
    if (input.country && input.gold && input.silver && input.bronze) {
     
      const exists = countries.some(
        (country) => country.country === input.country
      );
      if (exists) {
        alert("이미 존재하는 국가입니다. 업데이트를 해주세요 : )");
        return;
      }

      setCountries([...countries, { ...input }]);
      setInput({ country: "", gold: "", silver: "", bronze: "" });
    } else {
      alert("모두 입력해주셔야 추가가 가능합니다 : )");
    }
  };

국가 추가 부터 보자면,
➡️ if (input.country && input.gold && input.silver && input.bronze)하나라도 빈 칸이 있으면 안되게끔 조건을 걸어 주었고,
➡️ 중복되는 국가가 있는지 체크 해주었다.
➡️ 그리고 이미있는 나라들을 전개 구문을 사용, 새로 인풋되는 나라들을 배열에 넣었다.
➡️ 그 뒤에 추가가 되면 초기화 ❗❗❗❗❗

(( 하면서 느끼지만 전개 구문과 구조 분해 할당을 수도없이 강조 한 이유를 알겠다 ))

  • 메달 집계 업데이트 기능

  • 이미 추가된 국가의 메달 수를 수정할 수 있는 기능을 추가합니다.
  • 구체적인 구현:
    • 수정하려는 국가의 데이터를 입력한 후 [업데이트] 버튼을 클릭하면 해당 국가의 메달 수가 변경됩니다.
    • 입력한 나라 이름을 기반으로 리스트에서 해당 국가를 찾아 수정합니다.
const updatedCountries = [...countries];

    updatedCountries[countryIndex] = {
      ...updatedCountries[countryIndex],
      gold: input.gold || updatedCountries[countryIndex].gold,
      silver: input.silver || updatedCountries[countryIndex].silver,
      bronze: input.bronze || updatedCountries[countryIndex].bronze,
    };

    setCountries(updatedCountries);
    alert("국가 정보가 업데이트되었습니다 : )");
    setInput({ country: "", gold: "", silver: "", bronze: "" });
  };

이 부분은 더 간단하게 리펙토링 할 예정이지만 일단 올린다 😊
기존 나라들을 얕은 복사라고 해야할까? 넣어주고,
새롭게 들어가는 나라들이 있으면 넣어주고 없으면 둬 ! 해주고 싶어서 OR연산자를 사용해주었다 (||)

그리고 업데이트를 마치면 다시 초기화 ❗❗❗

  • 메달 집계 리스트 출력

  • App.jsx에 국가별 메달 집계를 표시하는 리스트를 추가합니다.
  • 국가 리스트 데이터를 저장할 useState를 추가하고, map 메서드를 사용해 리스트에 데이터를 반복 출력합니다.
  • 금메달 수를 기준으로 내림차순 정렬하여 상위 국가부터 표시되도록 합니다.

아 이부분 할 때 표를 너무 오랜만에 만들어봐서 다시 책 폈다.
표를 어떻게 그리더라 하면서 (ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ)

import React from "react";

const MedalList = ({ countries, onDelete }) => {
  const sortedCountries = [...countries].sort((a, b) => {
    const totalA = Number(a.gold);
    const totalB = Number(b.gold);
    return totalB - totalA;
  });

  return (
    <div className="medal-list-container">
      <table className="list-table">
        <thead>
          <tr>
            <th>국가명</th>
            <th>금메달</th>
            <th>은메달</th>
            <th>동메달</th>
            <th>합계</th>
            <th>액션</th>
          </tr>
        </thead>
        <tbody>
          {sortedCountries.map((country, index) => {
            const totalMedals =
              Number(country.gold) +
              Number(country.silver) +
              Number(country.bronze);

            return (
              <tr key={index}>
                <td>{country.country}</td>
                <td>{country.gold}</td>
                <td>{country.silver}</td>
                <td>{country.bronze}</td>
                <td>{totalMedals}</td>
                <td>
                  <button
                    onClick={() => onDelete(index)}
                    className="delete-btn"
                  >
                    삭제
                  </button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>

      {countries.length === 0 && (
        <p className="empty-country">등록된 국가가 없습니다 : )</p>
      )}
    </div>
  );
};

export default MedalList;

이 부분은 진짜 큰 어려움은 없었다. 나는 매달의 총 합계까지 만들어 주었다.

➡️ const sortedCountries = [...countries].sort((a, b) => { const totalA = Number(a.gold); const totalB = Number(b.gold); return totalB - totalA; });

이렇게 금메달의 개수로 내림차순을 주었고 !

➡️ .map을 사용해서 새롭게 정렬 해주는 방법을 선택했다.
➡️ 그리고 나라 배열이 비면 등록된 나라가 없다고 적어주기!

  • 메달 집계 삭제 기능

  • 각 나라 옆에 삭제 버튼을 추가하여, 클릭 시 해당 국가의 메달 집계가 리스트에서 제거되도록 합니다.
  • 구체적인 구현:
    • filter 메서드를 활용하여 선택된 국가를 제외하고 나머지 국가들로 리스트를 재구성합니다.
const [countries, setCountries] = useState([]);

  const handleDelete = (index) => {
    setCountries(countries.filter((_, i) => i !== index));
  };

➡️ 초기값은 빈 배열로 주고
➡️ 삭제 하려는 배열의 인덱스를 매개변수로 주었다.
➡️ 여기서 _는 사용하지 않는 첫 번째 매개변수
➡️ i는 현재 처리 중인 요소의 인덱스
➡️ i !== index는 삭제하려는 인덱스와 다른 모든 항목을 유지한다❗

  • 컴포넌트 구조 분리

  • 리스트 항목과 폼을 별도의 컴포넌트로 분리하여 코드의 가독성을 높입니다.
  • 예를 들어, 입력 폼은 MedalForm 컴포넌트로, 국가별 메달 정보는 MedalList와 MedalItem 컴포넌트로 나누어 관리할 수 있습니다.

나는 애초에 이걸 나눠서 했다 !

완성본


😣 이번 프로젝트에서 아쉬운 점
1. useEffect나 emotion을 배우지 못해서 사용하지 못한 점
2. 좀 더 코드를 가독성이 좋게 짜지 못한 점

😊 이번 프로젝트에서 잘한 점
1. 전개 구문, 구조 분해 할당, 통합 이벤트 관리
2. 배열 메서드를 적절하게 사용 한 점

profile
끄적 끄적 배운 걸 적습니다 / FRONT-END STUDY VELOG

0개의 댓글