[24.08.09] 리액트 개인 프로젝트 1일차 - 올림픽 메달 트래커 만들기

리액트 입문 주차에 들어가면서 개인 프로젝트가 시작됐다. 나라 이름과 메달 개수를 입력하면 금메달 수를 기준으로 내림차순 정렬되는 메달 트래커 사이트다!

요즘 바빠서 짧은 영상이나 메달 소식만 조금씩 전해듣고 있지만.. 반가운 기분이었다 ㅋㅋㅋ

 

큰 틀 나눠보기

우선 주어진 요구사항들을 보며 큰 덩어리들을 나눠보았다.

 

여러 필드의 값 하나로 합치기

입력 필드들을 여러 컴포넌트로 나누고나니까 입력 값을 받아오는게 헷갈렸다..

일단 시도해본 방법

-> 새로운 입력을 받을 state를 만들어서 값 변할 때마다 state 변하게하기

-> 최종 확정인 버튼을 누르면 그 state(새로운 입력)을 기반으로 아래 ranking에 보여줄 state 업데이트하기

 

const [newMedalData, setNewMedalData] = useState({});
return (
  <main>
    <h1 id="title">2024 파리 올림픽</h1>
    <section id="userInput">
      <MedalInputField
        type={"country"}
        newMedalData={newMedalData}
        setNewMedalData={setNewMedalData}
      >
        국가명
      </MedalInputField>
      // 생략
  </main>
);
function MedalInputField({ type, children, newMedalData, setNewMedalData }) {
  const inputHandler = (event) => {
    let newInput = newMedalData;
    newInput[`${type}`] = event.currentTarget.value;
    setNewMedalData(newInput);
    console.log(newMedalData);
  };

  return (
    <div style={medalInputFieldStyle}>
      <h3>{children}</h3>
      <input
        type="text"
        id={type}
        style={inputFieldStyle}
        onChange={inputHandler}
      ></input>
    </div>
  );
}

우와

 

state와 input field value 연동시키기

<div style={medalInputFieldStyle}>
  <h3>{children}</h3>
  <input
    type="text"
    id={type}
    style={inputFieldStyle}
    onChange={inputHandler}
    value={newMedalData[`${type}`]}
  ></input>
</div>

input field의 value와 업데이트 된 state가 연동될 수 있도록 value에 state 지정해준다.

 

메달 수에는 숫자만 입력되도록 하기

dataType을 확인해서 input의 type 속성을 변경

<input
  type={dataType === "country" ? "text" : "number"}
  id={dataType}
></input>

 

 

 

기본 상태 0, 국가명 placeholder 설정

value={medalDataInput[`${dataType}`] || defaultValue}
placeholder={dataType === "country" ? "국가 입력" : ""}

필수 구현사항은 아니었지만 이번엔 내 임의로 처리하는 것보다는 요구사항을 맞추듯이 제시된 예시와 최대한 똑같게 만들어보자 생각해서 위와같이 넣어주었다.

 

state 자체는 업데이트되는데, input field의 값이 변하질 않는다.

 

value={medalDataInput[`${dataType}`]}

혹시나해서 || defaultValue 부분을 없애고 medalDataInput[] 부분만 남겨뒀더니 input field의 값이 변한다..

그래서 그냥 초기값을 지정해줬다.

 

const [medalDataInput, setMedalDataInput] = useState({
  country: "",
  gold: 0,
  silver: 0,
  bronze: 0,
});

그래도 안된다..

 

const inputHandler = (event) => {
  let input = medalDataInput;
  input[`${dataType}`] = event.currentTarget.value;
  event.currentTarget.value = input[`${dataType}`];
  setMedalDataInput(input);
  console.log("medalDataInput: ", medalDataInput);
};

아예 onChange 이벤트로 등록해둔 inputHandler에서 currentTarget.value를 바꿔주니 해결이 되긴한다..

 

아무리 생각해도 너무 돌아가는 것 같아서 천천히 정리해보았는데.. 정리를 시작하자마자 실수를 깨달았다 ㅋㅋㅋㅋㅋㅋ

지금 medalDataInput 자체는 {country: "국가이름", gold: 0, ...} 의 구성인 객체인데 참조타입을 구조분해할당 등으로 내부 값만 복사해오는 것이 아니라 참조 자체를 복사해와서 뭔가 이상하게 동작한 것이었다......!!!!!!!!

 

const inputHandler = (event) => {
  let input = { ...medalDataInput };
  input[`${dataType}`] = event.currentTarget.value;
  setMedalDataInput(input);
};

return (
  <div style={medalInputFieldStyle}>
    <h3>{children}</h3>
    <input
      type={dataType === "country" ? "text" : "number"}
      id={dataType}
      style={inputFieldStyle}
      onChange={inputHandler}
      value={medalDataInput[`${dataType}`] || defaultValue}
    </input>
  </div>
);

이렇게 바꿔주니까 깔끔하게 된다..ㅎㅎㅎㅎㅎ

 

 

국가추가 버튼을 입력했을 때 아래에 뜨도록 하기

원래는 div들을 정렬하려했는데.. table 태그가 있다는 것을 알아서 사용해보기로 했다

읽었을 때 더 확실하게 테이블 형식으로 관리되고 있다는 걸 알 수 있으니까!

<section id="ranking">
  <div className="ranking" id="rankingHeader"></div>
  <div className="ranking rankingItem"></div>
</section>
<table>
        <tr>
          <th scope="col">국가명</th>
          <th scope="col">금메달</th>
          <th scope="col">은메달</th>
          <th scope="col">동메달</th>
          <th scope="col">액션</th>
        </tr>
        <tr>
          <td>대한민국</td>
          <td>3</td>
          <td>3</td>
          <td>3</td>
          <td>
            <button></button>
          </td>
        </tr>
        // 생략
</table>

테스트~ 잘 들어간다

한 국가의 메달 정보를 담을 <tr>을 컴포넌트로 넣어서 관리해주면 될 듯 하다

 

 

table에서 <tr> 태그가 보이지 않을 수 있으니 <tbody>, <thead>, <tfoot> 등을 사용하라고 한다.

 

<table>
  <thead>
    <tr>
      <th scope="col">국가명</th>
      <th scope="col">금메달</th>
      <th scope="col">은메달</th>
      <th scope="col">동메달</th>
      <th scope="col">액션</th>
    </tr>
  </thead>
  <tbody>
    {medalData.map((data) => {
      return <MedalTableRow data={data}></MedalTableRow>;
    })}
  </tbody>
</table>

이렇게 변경해주었다.

 

 

input 태그 여러개일 때 버튼으로 입력 받기

예시 사이트도 그렇게 구현되어있고, 나도 메달 정보를 입력하고나면 습관적으로 엔터를 쳐서 엔터를 쳤을 때도 테이블에 데이터를 추가할 수 있도록 기능을 추가해보기로 했다.

 

첫 시도

const enterHandler = (event) => {
    if (event.key === "Enter") {
      updateButtonHandler();
    }
  };

버튼에 onKeyDown 이벤트를 설정했다. 아무 동작이 일어나지 않았다.

 

엔터키로 전체 입력을 제출할 수 있도록 여러 input 태그들이 있었던 section을 form으로 바꿔주었고, 엔터를 눌렀을 때 자동으로 submit 되므로 onSubmit에서 기본 동작(리로드)를 막았다!

그런데 버튼의 onClick 동작을 onSubmit으로 옮기지 않고도 원하는 동작이 실행됐다. 버튼에 type="submit"을 지정해줬기 때문인데, submit 버튼은 form에서 submit이 발생했을 때 클릭이벤트를 호출한다고 한다.

폼 태그 내 버튼이 하나 더 있을거라 이걸 어떻게 구분할지 고민하고 있었는데 그 버튼은 type="button"으로 지정해주면 되겠다싶었다.

 

 

이미 입력된 나라의 정보는 추가가 아니라 수정하기

const updateButtonHandler = () => {
  let updatedMedalData = [];
  if (children === "업데이트") {
    const updateCountryData = medalData.find(
      (data) => data.country === medalDataInput.country
    );
    
    for (let key in medalDataInput) {
      updateCountryData[key] = medalDataInput[key];
    }
    updatedMedalData = [...medalData];
  } else {
    updatedMedalData = [...medalData, medalDataInput];
  }
  
  setMedalDataInput({});
  setMedalData(updatedMedalData);
};

1) children을 이용해 국가 추가 버튼이 아닌 업데이트 버튼인지 확인한다.

-> 이 부분때문에 국가 추가/업데이트 버튼을 같은 컴포넌트가 아닌 다른 컴포넌트로 분리해야할지 고민이 된다..

- 같은 컴포넌트로 분리했던 이유: 같은 state 를 사용함 입력받은 데이터로 테이블을 업데이트 시킨다는 의미가 같음

- 고민되는 이유: 같은 state를 사용하고 유사한 동작을 수행하지만, 결국 입력받은 데이터를 처리하는 과정은 다르다보니 다른 동작 같기도 함 / 모든 태그를 컴포넌트로 분리해야하는 것이 아닌 것 같으니 차라리 form 태그 부분을 분리 혹은 유지하고 form 태그 내에 각각 버튼 태그로 있어도 충분하지 않나 싶기도 함.. 

2) 저장돼있는 state인 medalData에서 입력받은 데이터의 country와 같은 country 를 갖는 객체를 찾는다

3) 메달 정보 객체의 키들을 돌면서 업데이트 해줘야하는 나라의 메달수를 입력받은 데이터로 변경해준다.

4) 최종 수정본인 메달 정보(객체)들의 배열을 state로 업데이트 해준다.