본문 바로가기
Front-End/React

[ React ] React 기본 ③ React에서 배열 사용하기 - 4. 리스트 데이터 수정하기

by 2CHAE._.EUN 2022. 10. 1.

1. 배열을 이용한 React의 List에 아이템을 동적으로 수정해보기

 

① 수정하기 버튼을 위한 State 추가하기

 

const [isEdit, setIsEdit] = useState(false);

 

isEdit의 기본값은 false이다. isEdit의 역할은 true와 false 값을 갖게 되어 수정 중인지 수정 중이 아닌지에 대해

boolean형으로 체크를 한다. true라면 수정 중이기 때문에 코드를 작성하면 되고, false라면 버튼만 랜더하면 된다.

 

 

② isEdit이 가지고 있던 값을 반전시키는 toggleEdit 함수 추가하기

 

const toggleIsEdit = () => setIsEdit(!isEdit);

 

toggleEdit 함수는 호출이 되는 순간 원래 isEdit이 가지고 있던 값을 반전시킨다. toggleEdit 함수가 호출이 되면

setIsEdit이 호출이 되면서 not 연산을 통해 isEdit이 true였다면 false로 바뀌는 반전 연산이 작동한다.

 

즉 수정하기 버튼을 클릭하게 되면 수정하기에 대한 isEdit state가 false에서 true로, true에서 false로 계속 바뀌게

된다.

 

 

③ isEdit state 값에 따른 행동 추가하기

 

<div className="content">
        {isEdit ? (
          <>
            <textarea />
          </>
        ) : (
          <>{content}</>
        )}
</div>
<button onClick={handleRemove}>삭제하기</button>
<button onClick={toggleIsEdit}>수정하기</button>

 

isEdit state가 true일 경우 textarea 화면이 나오고 false라면 저장된 content가 랜더링 된다.

 

수정 폼에 입력하는 데이터들도 state로 핸들링할 수 있도록 만들어야 한다.

 

const [localContent, setLocalContent] = useState("");

 

 <div className="content">
        {isEdit ? (
          <>
            <textarea
              value={localContent}
              onChange={(e) => setLocalContent(e.target.value)}
            />
          </>
        ) : (
          <>{content}</>
        )}
</div>

 

 

④ 내용을 수정했을 경우 수정된 내용을 저장하는 버튼을 추가하기

 

버튼을 isEdit state에 따라서 랜더링해주면 된다. isEdit state가 true라면 수정 취소와 수정 완료 버튼을 랜더링하면

되고, false라면 수정하기와 삭제하기 버튼을 랜더링하면 된다.

 

</div>
      {isEdit ? (
        <>
          <button onClick={toggleIsEdit}>수정취소</button>
          <button>수정완료</button>
        </>
      ) : (
        <>
          <button onClick={handleRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </>
)}

 

수정하기 버튼을 클릭할 경우 입력 수정 폼에 아무것도 출력이 되지 않는다. 그래서 수정 폼에 원본 데이터를 출력하는

기능을 추가하기 위해서는 수정 폼의 state인 localContent의 기본값에 기존에 저장된 content를 넣어주면 된다.

 

const [localContent, setLocalContent] = useState(content);

 

기본 값으로 넣어준다면 수정 폼에 매칭되는 localContent state가 기본값으로 출력이 되고 content가 설정됐기

때문에 자동으로 수정하기 이전의 상태가 나타나게 된다. 하지만 수정 취소를 할 경우 수정이 덜 완료된 값이 저장되는

문제점이 발생하게 된다. 수정 폼을 관리하는 state는 content가 아니라 localContent state이기 때문에 수정하기 버튼을

클릭해서 content를 수정할 경우 수정 취소를 눌렀다가 수정 하기를 눌렀다고 해서 그 값이 초기화 되지 않는다.

 

그래서 수정이 덜 완료된 수정 폼을 초기화시키는 함수를 추가해야한다. 

 

const handleQuitEdit = () => {
    setIsEdit(false);
    setLocalContent(content);
  };

 

</div>
      {isEdit ? (
        <>
          <button onClick={handleQuitEdit}>수정취소</button>
          <button>수정완료</button>
        </>
      ) : (
        <>
          <button onClick={handleRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </>
      )}

      <br />
</div>

 

handleQuitEdit 함수는 수정 취소를 누를 경우 isEdit state가 수정 중이지 않은 false로 돌아가게 되고 수정 폼의

state인 localContent의 기본 값도 다시 기존에 작성해 놓은 content로 돌아가게 된다.

 

 

⑤ 수정 완료를 눌렀을 때 일기 아이템이 수정되도록 만들기

 

리액트의 특성 상 데이터는 위에서 아래로, 이벤트는 아래에서 위로 이동하기 때문에 수정 완료에 대한 기능은

DiaryItem.js에서 App 컴포넌트까지 전달하기 위해서는 데이터를 가지고 있는 App 컴포넌트에 수정 하는 기능을 

하는 함수를 만들고 DiaryItem 컴포넌트까지 전달해줘야한다.

 

수정하는 함수인 onEdit은 매개변수로 뭘 수정할지 받아와야한다. 어떤 아이디를 가진 일기를 수정할 것인지에 대해

targetId와 어떻게 content를 변경시킬 건지에 대한 newContent를 매개변수로 받아준다.

 

onEdit 함수는 일기 데이터 배열을 저장하고 있는 data에 대한 상태 함수인 setData()를 호출해서 data의 값을

변경해준다. 우선 data에 map 함수를 적용해서 data에 저장돼있는 모든 일기들이 매개변수로 전달받은 targetId와

일치한지 확인해준다. 

* map 함수 : 배열의 원본 요소를 순회함과 동시에 연산을 수행함으로써 리턴되는 값들만 따로 배열에 추려내

새로운 배열을 반환할 수 있게끔 해주는 함수이다.

 

id가 일치하는 일기는 수정 중인 일기 딱 하나밖에 없기 때문에 spread 연산자를 사용해 원본 데이터를 다 불러와준

다음에 기존에 저장돼있던 content를 수정된 값인 newContent로 바꿔주면 된다. id가 일치하지 않으면 수정 대상이

아니기 때문에 그대로 기존에 저장된 data를 반환하면 된다.

* spread 연산자는 반드시 원래 있던 state를 먼저 펼쳐주고, 변경하고자 하는 state의 프로퍼티를 마지막에 작성해야

한다. 

 

App.js

const onEdit = (targetId, newContent) => {
    setData(
      data.map((it) =>
        it.id === targetId ? { ...it, content: newContent } : it
      )
    );
  };

 

onEdit 함수는 setData를 통해서 값을 전달한다. onEdit 함수는 특정 일기를 수정하는 함수이므로 매개변수로 받은

targetID를 갖는 일기 데이터를 일기가 저장돼있는 배열에서 수정할 것이기 때문에, 원본 일기 데이터 배열에 

자바스크립트 내장 함수인 map을 사용해서 모든 요소를 순회하면서 새로운 배열을 만들어서 setData에 전달하게 된다.

새로운 배열은 수정이 완료된 일기 배열을 의미한다. 

 

삼항 연산자를 이용하여 수정 대상이라면 기존에 저장돼있던 내용인 content를 newContent로 교체해주고 수정

대상이 아니라면 원래 있던 content값이 있는 데이터를 리턴해주면 된다. 매개변수로 전달받은 targetId와 일치하는

요소를 가진 그 객체의 값은 content가 newContent로 바뀌게되고 일치하지 않는 요소는 원본 값을 지키게 된다.

 

onEdit 함수는 결론적으로 수정 폼을 가지고 있는 DiaryItem이 직접 호출을 해야하기 때문에 Prop으로 전달해준다.

 

DiaryItem 컴포넌트에서는 수정 완료 버튼을 눌렀을 때 발생하는 이벤트를 처리할 함수를 생성한다.

handleEdit에는 수정할 일기의 id와 새로 바뀌는 내용의 localContent를 전달해주면 된다. 

 

DiaryItem.js

  const handleEdit = () => {
    onEdit(id, localContent);
  };

 

수정할 데이터에 대한 조건을 검사해야한다. DiaryEditor 컴포넌트에서 content의 길이가 1 미만일 경우 저장이

안되도록 막아놨기 때문에 수정된 데이터에 대해서도 길이를 확인해야 한다. 

 

const handleEdit = () => {
    if (localContent.length < 1) {
      localContentInput.current.focus();
      return;
    }

    if (window.confirm(`${id}번째 일기를 수정하시겠습니까?`)) {
      onEdit(id, localContent);
      toggleIsEdit(); // 일기를 수정 완료하면 수정 폼을 다시 false로 만들어서 닫아주기
    }
  };
  
<textarea
	ref={localContentInput}
	value={localContent}
	onChange={(e) => setLocalContent(e.target.value)}
/>

 

* onChange는 이벤트의 값이 변경되었을 때 수정해는 이벤트이다. onChange에 등록되는 콜백함수는 이벤트 객체인

매개변수 e를 전달받게 되고 수정 폼의 값이 바뀌었을 때 onchange prop에 전달한 콜백 함수를 수행하게 된다.

즉, 값이 바뀔 때마다 onchange에 전달한 콜백함수가 계속해서 호출이 된다.

 


 

App.js

import { useRef, useState } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";

function App() {
  const [data, setData] = useState([]);
  // data state는 일기 데이터 배열을 저장하기 때문에 배열로 초기값을 만들어준다.

  const dataId = useRef(1); // ID는 1부터 시작

  const onCreate = (author, content, emotion) => {
    // 일기 데이터 추가 함수

    const created_date = new Date().getTime();

    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };

    dataId.current += 1;
    setData([newItem, ...data]); // 기존에 존재하던 데이터에 새로운 일기를 추가
  };

  const onDelete = (targetId) => {
    const newDirayList = data.filter((it) => it.id !== targetId);
    setData(newDirayList);
  };

  const onEdit = (targetId, newContent) => {
    setData(
      data.map((it) =>
        it.id === targetId ? { ...it, content: newContent } : it
      )
    );
  };

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <DiaryList onEdit={onEdit} diaryList={data} onDelete={onDelete} />
    </div>
  );
}

export default App;

 

DiaryEditor.js

import { useRef, useState } from "react";

const DiaryEditor = ({ onCreate }) => {
  const [state, setState] = useState({
    author: "",
    content: "",
    emotion: 1,
  });

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const authorInput = useRef();
  const contentInput = useRef();

  const handleSubmit = () => {
    if (state.author.length < 1) {
      authorInput.current.focus();
      return;
    }

    if (state.content.length < 1) {
      contentInput.current.focus();
      return;
    }

    onCreate(state.author, state.content, state.emotion); // 새로운 일기 데이터 추가

    alert("저장 성공");

    setState({
      // 입력 폼에 남아있는 데이터를 초기화
      author: "",
      content: "",
      emotion: 1,
    });
  };

  return (
    <div className="DiaryEditor">
      <h2>오늘의 일기</h2>
      <div>
        <input
          ref={authorInput}
          name="author"
          value={state.author}
          onChange={handleChangeState}
        ></input>
      </div>
      <div>
        <textarea
          ref={contentInput}
          name="content"
          value={state.content}
          onChange={handleChangeState}
        ></textarea>
      </div>
      <div>
        <select
          name="emotion"
          value={state.emotion}
          onChange={handleChangeState}
        >
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
          <option value={4}>4</option>
          <option value={5}>5</option>
        </select>
      </div>
      <div>
        <button onClick={handleSubmit}>일기 저장하기</button>
      </div>
    </div>
  );
};

export default DiaryEditor;

 

DiaryList.js

import DiaryItem from "./DiaryItem";

const DiaryList = ({ diaryList, onDelete, onEdit }) => {
  return (
    <div className="DiaryList">
      <h2>일기 리스트</h2>
      <h4>{diaryList.length}개 일기가 있습니다.</h4>
      <div>
        {diaryList.map((it) => (
          <DiaryItem key={it.id} {...it} onDelete={onDelete} onEdit={onEdit} />
        ))}
      </div>
    </div>
  );
};

DiaryList.defaultProps = {
  //undefined 방지용으로 기본값 설정
  diaryList: [],
};

export default DiaryList;

 

DiaryItem.js

import { useRef, useState } from "react";

const DiaryItem = ({
  author,
  content,
  emotion,
  created_date,
  id,
  onDelete,
  onEdit,
}) => {
  const handleRemove = () => {
    if (window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)) {
      onDelete(id); // '예'일 경우 삭제 함수에 id를 전달해준다.
    }
  };

  const [isEdit, setIsEdit] = useState(false);

  const toggleIsEdit = () => setIsEdit(!isEdit);

  const [localContent, setLocalContent] = useState(content);

  const handleQuitEdit = () => {
    setIsEdit(false);
    setLocalContent(content);
  };

  const localContentInput = useRef();

  const handleEdit = () => {
    if (localContent.length < 1) {
      localContentInput.current.focus();
      return;
    }

    if (window.confirm(`${id}번째 일기를 수정하시겠습니까?`)) {
      onEdit(id, localContent);
      toggleIsEdit(); // 일기를 수정 완료하면 수정 폼을 다시 false로 만들어서 닫아주기
    }
  };

  return (
    <div className="DiaryItem">
      <div className="info">
        <span>
          작성자 : {author} | 감정 : {emotion}
        </span>
        <br />
        <span className="date">{new Date(created_date).toLocaleString()}</span>
      </div>
      <div className="content">
        {isEdit ? (
          <>
            <textarea
              ref={localContentInput}
              value={localContent}
              onChange={(e) => setLocalContent(e.target.value)}
            />
          </>
        ) : (
          <>{content}</>
        )}
      </div>
      {isEdit ? (
        <>
          <button onClick={handleQuitEdit}>수정취소</button>
          <button onClick={handleEdit}>수정완료</button>
        </>
      ) : (
        <>
          <button onClick={handleRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </>
      )}

      <br />
    </div>
  );
};

export default DiaryItem;