부트캠프 프로젝트(完)/올림픽 메달 집계 사이트(完)

[올림픽 메달 집계 사이트] 1. 입력창,데이터 뿌리기,삭제,중복x,업데이트

민ズl 2024. 10. 30. 15:32

이번 프로젝트는 드디어 리액트로! 

익숙하던 CRA대신 vite로 셋팅👀

1. 입력창

먼저 입력하는 부분을 map으로 돌리기!

const formArrName = ["country", "gold", "silver", "bronze"];
return (
	...
    <div className="flex w-full gap-5">
        {formArrName.map((name, i) => (
          <div className="flex gap-2" key={i}>
            <span className="font-bold">{name}</span>
            <input
              type={i === 0 ? "text" : "number"}
              name={name}
              value={inputValues[name]}
              onChange={handleChange}
              className={i === 0 ? "" : "w-10"}
              placeholder={i === 0 ? "Enter country" : "0"}
            />
          </div>
        ))}
    </div>
)

 

2. 입력한 데이터 뿌리기

그리고 입력한 데이터를 담을 useState 하나,

데이터들을 저장할 useState 하나 만든후

const [inputValues, setInputValues] = useState({ // 입력한 데이터를 담음
    country: "",
    gold: 0,
    silver: 0,
    bronze: 0,
  });
 const [medalList, setMedalList] = useState([]); // 데이터를 저장

 

input의 onChange와 저장button의 onClick으로 연결하기!

리액트에선 불변성을 유지해야하기 때문에 (참고) spread연산자로 가져오기🌟

 

[React]컴포넌트,props,불변성

리액트에서 컴포넌트는 중요한 개념🌟개념적으로 컴포넌트는 javascript 함수와 유사! props라는 input을 받아서 react 앨리먼트라는 output을 반환 🔸리액트 컴포넌트 표현방법1. 함수형 컨포넌트(권

bom-na.tistory.com

그리고 id값은 중복안되는 값으로 현재시간으로 넣어주고, 나라입력을 안했을시, 메달갯수가 0개미만일때 alert창띄우고 return해버리기!

submit후에 input창 value초기화~!

const handleChange = (e) => {
    const { name, value } = e.target;
    setInputValues((prevData) => ({
      ...prevData,
      [name]: value,
    }));
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    const { country, gold, silver, bronze } = inputValues;
    
    if (country === "") {
      alert("나라를 입력해주세요");
      return;
    }
    if (gold < 0 || silver < 0 || bronze < 0) {
      alert("메달 갯수를 0개 이상으로 입력해주세요");
      return;
    }
    const newMedalList = {
      id: new Date().getTime(),
      inputValues,
    };
    setMedalList([...medalList, newMedalList]);
    setInputValues({
      country: "",
      gold: 0,
      silver: 0,
      bronze: 0,
    });
  };
 ...
 return(
 ...
   <button type="submit" onClick={handleSubmit} className="add__btn">
      Add
   </button>
 )

 

근데 나는 total도 넣고싶었음! 그래서 state값 inputValues의 value를 Object.values()가져오고,

맨 처음을 slice로 제외한후(나라명은 string임), reduce로 각각 더해줌(string으로 변환됐기때문에 Number메서드로 변환)

const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValues.country === "") {
      alert("나라를 입력해주세요");
      return;
    }
    const newMedalList = {
      id: new Date().getTime(),
      inputValues,
      total: Object.values(inputValues)
        .slice(1)
        .reduce((acc, cur) => acc + Number(cur), 0),
    };
    setMedalList([...medalList, newMedalList]);
    setInputValues({
      country: "",
      gold: 0,
      silver: 0,
      bronze: 0,
    });
  };

 

그리고 마찬가지로 map으로 데이터 뿌려주기!

<tbody>
    {medalList &&
      medalList.map((data) => (
        <tr key={data.id}>
          <td>{data.inputValues.country}</td>
          <td>{data.inputValues.gold}</td>
          <td>{data.inputValues.silver}</td>
          <td>{data.inputValues.bronze}</td>
          <td>{data.total}</td>
          <td>
            <button type="button">Delete</button>
          </td>
        </tr>
      ))}
</tbody>

 

*잠깐의 리팩토링*

  • 파일분리 : 입력컴포넌트를 MedalForm으로 빼고, 보여주는 UI를 MedalList컴포넌트로 뺌, 그리고 공통의  const [medalList, setMedalList] = useState([])를 App.jsx로 뺌
  • 네이밍 변경 : const [medalList, setMedalList] = useState([])을
    const [medalItems, setMedalItems] = useState([]) 으로 네이밍 변경

3. 데이터를 뿌릴때 금메달 갯수로 정렬

sort를 사용하여 정렬

{medalItems &&
   medalItems.sort(
     (a, b) => b.inputValues.gold - a.inputValues.gold).map((data) => (
        <tr key={data.id}>
            <td>{data.inputValues.country}</td>
            <td>{data.inputValues.gold}</td>
            <td>{data.inputValues.silver}</td>
            <td>{data.inputValues.bronze}</td>
            <td>{data.total}</td>
            <td>
              <button type="button">Delete</button>
            </td>
       </tr>
      )
   )
}

 

4. 저장한 데이터가 없을때 vs 있을때

{medalItems.length === 0 ? (
    <tr>
      <td className="border-none" colSpan={6}>
        <p className="text-softly-300 font-normal text-center py-5">
          아직 추가 되지 않았습니다.
        </p>
      </td>
    </tr>
  ) : (
    medalItems
      ...
  )}

🚫 리액트 컴포넌트에서는 조건문이 삼항연산자만 가능

 

5. 삭제 버튼구현

filter로 해당 데이터 id와 저장된 데이터id와 비교하여 일치하지 않는것만 가져와서 뿌리기!

const handleDelete = (deleted) => {
	setMedalItems(medalItems.filter((t) => t.id !== deleted.id));
};
...
<button type="button" onClick={() => handleDelete(data)}>
   Delete
</button>

여기서 onClick에 () => 의 함수호출이 헷갈렸음,,,

onClick(handleDelete(data))를 호출시 onClick이벤트에 id값을 받는 인자가 전달이 안됨

onClick(()=>handleDelete(data))로 호출시에만 버튼 클릭시 data 객체가 handleDelete의 deleted 매개변수로 전달됨!

(리액트에서는 전달할 인자가 있으면 ()=>handleDalete(data)로!)

6. 중복 작성한 나라는 저장 금지

forEach를 쓰면서 데이터를 돌리고 조건문을 쓰니까 return이 안됨(forEach는 조건에 만족해도 데이터를 다 돌림)

some을 사용하여 해당 조건이 만족하면 break

const isDuplicate = medalItems.some(
   (data) => data.inputValues.country === inputValues.country
);

if (isDuplicate) {
   alert("동일한 나라가 이미 리스트에 있습니다.");
   return;
}

 

7. 업데이트 구현

이미 작성한 리스트의 나라명을 입력하고 수정할 메달 갯수를 입력후 업데이트 버튼 클릭시, 해당 데이터로 수정됨

 

데이터가 담긴 setMedalItems안에서

1. 데이터를 map으로 돌린후

2. 저장된 데이터의 나라명과 수정할 나라명이 일치한지 확인후

3-1. true이면 spread연산자로 기존 데이터를 복사, 수정할 데이터로 수정
3-2. false이면 그대로 원본 데이터

setMedalItems((prev) =>
  prev.map((item) =>
    item.inputValues.country === country
      ? {
          ...item,
          inputValues: { ...inputValues },
          total: Object.values(inputValues)
            .slice(1)
            .reduce((acc, cur) => acc + Number(cur), 0),
        }
      : item
  )
);

*이때 덮어씌울때 spread연산자를 사용했는데 이부분에 대해서 이해가 잘 안가서 끄적여봄!

spread 연산자를 사용하면 얕은 복사를 통해 새로운 속성을 추가하거나 기존 속성을 덮어씌울수 있음

// 예시
const original = { a: 1, b: 2 };
const updated = { ...original, b: 3, c: 4 }; // updated = { a: 1, b: 3, c: 4 }

동일한 속성이 있을 경우, 가장 나중에 지정한 값이 우선적으로 적용🌟