TIL. 입력 값을 실시간으로 수정/ 삭제하기

ichbinmin2·2021년 2월 24일
0

React

목록 보기
22/25
post-thumbnail

👩🏻‍💻 Toy Project(Teta Card Maker)의 진행 기록 : 입력 form의 실시간 수정/삭제 기능 구현

오늘은 Toy Project인 [카드 메이커] 에서 가장 기본적인 기능을 구현해보려고 한다. 바로, card의 입력 form에서 입력한 값을 수정해 새로운 card로 만들어주는 기능이다. 단순히 값을 받아와 card를 추가해주는 것뿐만 아니라, 이미 입력폼에 작성을 하고 추가까지 한 card를 실시간으로 수정하고 업데이트하는 기능과 card를 삭제하는 기능까지 두가지를 구현해볼 것이다.

일단 CardEdit Component에 useRef 함수를 이용하여, 각각의 태그를 받아올 수 있도록 useRef()를 설정하고 각 태그마다 ref 값으로 연결했다.

const CardEdit = ({ card }) => {
  const formRef = useRef();
  const nameRef = useRef();
  const companyRef = useRef();
  const themeRef = useRef();
  const emailRef = useRef();
  const titleRef = useRef();
  const messageRef = useRef();

  const {
    id,
    name,
    company,
    theme,
    title,
    email,
    message,
    fileName,
    fileURL,
  } = card;

  return (
    <form ref={formRef} className={styles.writeform}>
      <input
        ref={nameRef}
        type="text"
        name="name"
        value={name}
        className={styles.input}
        onChange={onChange}
      ></input>
      <input
        ref={companyRef}
        type="text"
        name="company"
        value={company}
        className={styles.input}
        onChange={onChange}
      ></input>
      <select //
        ref={themeRef}
        name="theme"
        value={theme}
        className={styles.select}
        onChange={onChange}
      >
        <option value="light">light</option>
        <option value="dark">dark</option>
        <option value="colorful">colorful</option>
      </select>
      <input
        ref={titleRef}
        type="text"
        name="title"
        value={title}
        className={styles.input}
        onChange={onChange}
      ></input>
      <input
        ref={emailRef}
        type="email"
        name="email"
        value={email}
        placeholder="이메일 주소"
        className={styles.input}
        onChange={onChange}
      ></input>
      <textarea
        ref={messageRef}
        value={message}
        className={styles.message}
        onChange={onChange}
      ></textarea>
      <div className={styles.inputBtn}>
        <ImageInput />
      </div>
      <Button name="Delete" />
    </form>
  );
};

export default CardEdit;

그리고, onChange가 업데이트 될 때마다 onChange(event) 이벤트 함수를 실행할 수 있도록 연결해주었다.

const CardEdit = ({ card }) => {

const onChange = (event) => {
    if (event.currentTarget === null) {
      return;
    }
    event.preventDefault();
    // form의 새로고침을 방지
    
    updateCard({
      ...card, 
      [event.currentTarget.name]: event.currentTarget.value,
    });
  };

onChange 함수에서는 event를 인자로 받으며, 간단하게 조건식으로 currentTarget이 null 값이면 그대로 return 할 수 있게 해주었고, currentTarget의 값이 들어오면 updateCard 안에서 기존의 card의 key, value를 그대로 사용할 수 있도록 복사해온 뒤, event current target에 있는 name이 card의 key가 되고 event current target에 있는 현재 값value가 될 수 있도록 지정해주었다. 이제 form에 입력이 될 때마다 onChange가 value 값을 name으로 updateCard에 담아 업데이트 해줄 것이다.

업데이트를 하거나, 삭제해주는 기능의 함수는 해당 데이터를 총괄적으로 관리하는 상위 component에서 받아와야 하므로, 상위 component에서 함수를 작성을 한 뒤에 이 값을 필요한 component에 props로 받아오도록 설정했다. 물론, 업데이트 뿐만 아니라 delete 기능도 추가할 것이기 때문에 해당 기능을 수행할 함수인 deleteCard도 받아올 수 있도록 임의로 지정했다.

CardEdit Component의 바로 위에 있는 부모 Component(CardEditor)로 부터 전달 받을 수 있도록 props로 값을 받고

const CardEditor = ({ cards, onAddCard, updateCard, deleteCard }) => {
  return (
    <section className={styles.editorBox}>
      <h1 className={styles.title}>Card Maker</h1>
      <ul className={styles.editor}>
        {cards.map((card) => (
          <CardEdit 
						key={card.id}
            card={card}
            updateCard={updateCard}
            deleteCard={deleteCard} />
        ))}
        <CardAdd card={cards} onAddCard={onAddCard}/>
      </ul>
    </section>
  );
};

export default CardEditor;

그리고 다시 총괄적으로 관리해줄 최상위 부모 Component(CardMaker)로 부터 전달 받을 수 있도록 props에 값을 전달해주었다.

const onUpdateCard = (card) => {
    console.log(card);
  };

  const onDeleteCard = (card) => {
    console.log(card);
  };

  return (
    <section className={styles.cardMakerBox}>
      <CardEditor
        cards={cards}
        onAddCard={onAddCard}
        updateCard={onUpdateCard}
        deleteCard={onDeleteCard}
      />
      <CardPreview cards={cards} />
    </section>
  );
};

이제 가장 최상위 Component(CardMaker)에서 업데이트 기능을 담당하는 onUpdateCard와 삭제를 하는 기능인 onDeleteCard 함수를 작성한다. 이때 바로 아래 자식 Compoennt인 CardEditor에 각각의 함수를 props로 전달하는 것 또한 잊으면 안된다.

각각의 값을 제대로 받아오고 있는지 확인하기 위해서 각 함수마다 console.log를 입력하고, edit 칸에 작성을 해보았다.

입력하거나 카드를 삭제하는 함수를 받을 때 제대로 인식하여 업데이트 되는 것을 콘솔에서 확인할 수 있다.

아직까지는 실시간으로 입력한 것이 그대로 입력되지 않는다. 여기서 업데이트가 되지 않는 것은 edit form 이라는 것이 form으로 받은 이 card의 상태에 따라서 보여지도록 되어 있기 때문이다. 즉, state가 card 안에 들어있는 상태이다. 그래서 이 card가 업데이트하도록 만들어야지 update 되는 내용이 보여질 것이다. 그 말인 즉슨, 업데이트가 되면 state도 업데이트 해줘야 된다.

const [cards, setCards] = useState([
    {
      id: "1",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "light",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: null,
    },
    {
      id: "2",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "dark",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: "teta.png",
    },
    {
      id: "3",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "colorful",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: null,
    },
  ]);

card-maker.jsx에서 받아오는 데이터를 살펴보면 useState는 배열로 데이터를 담아두었음을 확인해볼 수 있다.

기존의 배열 안에서 관리되던 데이터를 오브젝트로 관리할 수 있도록 수정하였다. 또한 key 값은 해당 데이터의 id 값을 동일하게 써주고 value 값을 card 자체가 되도록 수정하였다.

const CardMaker = ({ authService }) => {
  const [cards, setCards] = useState({
    "1": {
      id: "1",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "light",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: null,
    },
    "2": {
      id: "2",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "dark",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: "teta.png",
    },
    "3": {
      id: "3",
      name: "Teta Min",
      company: "Kakao Company",
      theme: "colorful",
      title: "Software Engineer",
      email: "teta1dev@gmail.com",
      message: "Go for it",
      fileName: "teta",
      fileURL: null,
    },
  });

이 cards 라는 데이터는 오브젝트 형태로 관리를 할 수 있게 되었으니 이전에 배열의 형태로 map으로 뿌려주었던 식도 변화가 필요할 것이다.

const CardEditor = ({ cards, onAddCard, updateCard, deleteCard }) => {
  return (
    <section className={styles.editorBox}>
      <h1 className={styles.title}>Card Maker</h1>
      <ul className={styles.editor}>
        {Object.keys(cards).map((key) => (
          <CardEdit
            **key={key}**
            **card={cards[key]}**
            updateCard={updateCard}
            deleteCard={deleteCard}
          />
        ))}
        <CardAdd card={cards} onAddCard={onAddCard} />
      </ul>
    </section>
  );
};
const CardPreview = ({ cards }) => {
  return (
    <section className={styles.previewBox}>
      <h1 className={styles.title}>Card Preview</h1>
      <ul className={styles.card}>
        {Object.keys(cards).map((key) => (
          <CardBox key={key} card={cards[key]} />
        ))}
      </ul>
    </section>
  );
};

이제 실시간으로 업데이트 될 수 있도록 onUpdateCard 콜백 함수를 작성해보자.

const onUpdateCard = (card) => {
    const updated = { ...cards };
	// 기존에 있는 card 값을 그대로 복사해서 받아오고 
    updated[card.id] = card;
	// 업데이트되는 id key를 이용해서 오브젝트 전체를 변경해줄 것이다.
    setCards(updated);
	// 그리고 setCards 함수를 이용해서 업데이트를 설정해준다.
  };

해당 카드가 기존에 있는 데이터에서 실시간으로 수정(업데이트)이 되고 있음을 확인할 수 있다.


중요한 포인트!

state를 업데이트하는 함수를 사용할 때 이전 상태에 있는 것을 배경으로 해서, 무언가 값이 변경된 것을 업데이트 할 때는 component 안에 있는 state를 이용해서 업데이트 하게 되면 이 업데이트를 하는 시점에 존재하는 state가 기존의 다른 업데이트 값이 들어가지 않은 오래된 버전일 수도 있다. 즉, 동기적으로 사용하지 못할 때가 있다. 이것을 해결하기 위해서는 우리가 component 안에 있는 state에 의존해서 값을 변경하고 setCards로 업데이트하는 것이 아니라, setCards 라는 함수에서 사용할 수 있는 방법 중에 하나인 SetStateAction(콜백함수처럼 이용하는 방식)을 이용해볼 수 있을 것이다.

const onUpdateCard = (card) => {
    setCards((cards) => {
      const updated = { ...cards };
      updated[card.id] = card;
		// cards 안에 있는 card.id를 card로 선언하고
      return updated;
		// 이 값을 리턴한 것을 setCards에 넣어준다**
    });
  };

setCards를 부를 때의 상태(cards)를 그대로 가져와 updated 안에 복사하고 setCards id를 이용해서 오브젝트 안에 있는 key를 이용해 해당하는 그 key에 새로 업데이트 되는 card를 변경해준다. 그리고 return은 업데이트 되는 것들만 return 해주게 된다.

로직이 잘 이해가 되지 않을 땐 코드를 하나씩 뜯어보자.

const onUpdateCard = card => {
// card 라는 임의의 인자(콜백을 통해서 앞으로 사용할 데이터를 부르는 이름)를 받아온 뒤 
    setCards(cards => {
    // cards를 담은 setCards 안에서 
      const updated = { ...cards };
      // updated 안에 기존의 cards를 그대로 복사해서 넣어둔 뒤
      updated[card.id] = card;
      // 앞으로 받아올 card(임의의 인자) 안에 있는 id는 card라고 선언해주고
      return updated;
      // 그 변수를 리턴한 값을 
    });
    // setCards에 넣어준다.(업데이트 한다)
};

📌 createOrUpdateCard 로직의 순서

  1. object의 키값을 card의 id와 동일하게 해서 그 id와 동일한 키값을 가진 카드를 가지고 온다.
  2. 업데이트/추가를 한 다음에 추가가 된 updated 객체를 리턴한다.
  3. 그걸 다시 setState(setCards)를 통해 state에 변화를 일으켜 업데이트를 한다.

onAddCard나 onUpdateCard 함수나 처리해줘야 하는 것이 동일하기 때문에 하나의 함수(onAddOrUpdateCard(card))로 합쳐서 지정해준다.

const onAddOrUpdateCard = (card) => {
    setCards((cards) => {
      const updated = { ...cards };
      updated[card.id] = card;
      return updated;
    });
  };

return (
    <section className={styles.cardMakerBox}>
      <CardEditor
        cards={cards}
        onAddCard={onAddOrUpdateCard}
        updateCard={onAddOrUpdateCard}
        deleteCard={onDeleteCard}
      />
      <CardPreview cards={cards} />
    </section>
  );

edit에서 card를 지울 수 있는 onDeleteCard 함수도 설정해주자

const onDeleteCard = (card) => {
    setCards((cards) => {
      const updated = { ...cards };
      delete updated[card.id];
      return updated;
    });
  };

return (
    <section className={styles.cardMakerBox}>
      <CardEditor
        cards={cards}
        onAddCard={onAddOrUpdateCard}
        updateCard={onAddOrUpdateCard}
        **deleteCard={onDeleteCard}**
      />
      <CardPreview cards={cards} />
    </section>
  );

cardMaker Component의 바로 아래 자식인 cardEditor에 props로 전달해주고 이것을 다시 cardEdit으로 받아와 이벤트 함수 안에서 처리해주면 된다.

const CardEdit = ({ card, updateCard, deleteCard }) => {

	const onSubmit = () => {
	    deleteCard(card);
	};

return (
 .
 .
 .
<Button name="Delete" onClick={onSubmit} />
)

(지양해야하는 방식이긴 하지만) 이처럼 object는 동적으로 코딩이 가능하다. 또한, object는 데이터에 접근해서 직접적으로 데이터를 삭제할 수 있는 delete 명령어도 사용할 수 있기 때문에 상대적으로 간편하다.

구현 영상

profile
N개월차 프론트엔드 개발자, Teta Min

0개의 댓글