[React] useState( )로 배열 수정하기 (feat.일기장)

Hyun·2022년 1월 6일
0

React

목록 보기
10/22
post-thumbnail

배열을 이용한 React의 List에 아이템을 동적으로 수정하기 (with조건부렌더링)

🎯코딩 순서 정리

  1. DiaryItem.js에 가서 수정버튼을 생성한다.
  2. 수정버튼을 클릭하면 버튼은 수정취소,수정완료 버튼으로 바뀌고 수정완료버튼을 누르면 다시 수정하기,삭제하기의 버튼으로 돌아옴(if함수와 useStat함수 이용)
    즉, 기능을 만들어야하는 버튼은 수정하기,수정취소,수정완료 버튼이다.
  3. 수정하기버튼을 클릭하면 일기본문에 수정할수있도록 마우스커서가 위치하고 작성한 내용이 textarea에 뜨는기능
  4. 수정하기버튼을 눌렀다가 수정취소를 누르면 작성중이었던 내용이 뜨는기능
  5. 수정완료버튼을 누르면 수정중이었던 내용이 새로운내용으로 업데이트 되어야한다.

여기서, React의 특성상 데이터는 위에서 아래로의 단방향흐름으로 일어나고 이벤트는 아래에서 위로의 역방향흐름이 일어난다.

수정완료버튼을 생각해보면 새로운 이벤트가 발생한것이고 그러면 수정완료버튼이있던 DiaryItem.js컴포넌트에서 위로 부모컴포넌트인 App.js로 이벤트가 흐르게 되어야하므로 ➡️ App컴포넌트에 수정하면 일어나는 함수를 만들어서 그 함수를 App.js의 자식컴포넌트인 DiaryList.js에 prop을 주고 ➡️ 또 DiaryList.js의 자식컴포넌트인 DiaryItem.js에 prop을 주면 된다. (최종적으로 함수를 호출할곳은 DiaryItem.js)


💡사용한 개념

useState(), useRef(), props, onClick, !, onChange, 삼항연산자
다시한번 복습하는 시간을 갖자. 개념을 제대로 뿌셔버려야지🤩


📖예제

1. App.js

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

const App = () => {
  const [data, setData] = useState([]);
  const dataId = useRef(0);
  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 newDiaryList = data.filter((it) => it.id !== targetId);
    setData(newDiaryList);
  };

  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} onDelete={onDelete} diaryList={data} />
    </div>
  );
};

export default App;

<코드설명>

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

= 수정함수
무엇을 수정할지 모르기에 prop으로 전달되서 DiaryItem이 수행할 함수이기때문에 매개변수를 무엇을 수정할지 받아와야 한다.
그래서 어떤일기를 수정할것인지의 targetId, 내용을 어떻게 수정할건지의 newContent를 매개변수로 받아온다
setData를 호출해서 데이터의 값을 바꿔준다.
data에 map이라는 함수를 적용해서 각각 모든 요소들이 현재 매개변수로 전달받은 targetId와 일치한지 검사를한 후, true면 원본데이터를 모두 불러와준후 내용을 newContent로 수정해주고, false라면 it을 반환해줌
map이라는 내장함수는 모든요소를 순회하면서 새로운배열(수정완료된배열)을 만들어서 setData에 전달을 하게됨
onEdit함수는 수정하기 버튼을가진 DiaryItem이 호출해야한다

2)<DiaryList onEdit={onEdit} onDelete={onDelete} diaryList={data} />
= 자식컴포넌트인 DiaryList로 prop을 전달 (DiaryList는 DiaryItem의 부모컴포넌트)

2. DiaryList.js

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

export default DiaryList;

<코드설명>

1) const DiaryList = ({ onEdit, onDelete, diaryList })
= 부모인 App.js컴포넌트에서 onEdit을 prop으로 전달받음

2) <DiaryItem key={it.id} {...it} onEdit={onEdit} onDelete={onDelete} />
= 자식인 DiaryItem.js컴포넌트에 prop으로 또 전달

3. DiaryItem.js⭐️

import { useRef, useState } from "react";
👉🏻const DiaryItem = ({
  onEdit,
  onDelete,
  author,
  content,
  created_date,
  emotion,
  id,
}) => {
  👉🏻const [isEdit, setIsEdit] = useState(false);
  👉🏻const toggleIsEdit = () => setIsEdit(!isEdit);
  👉🏻const [localContent, setLocalContent] = useState(content);
  👉🏻const localContentInput = useRef();

  const handleRemove = () => {
    if (window.confirm(`${id + 1}번째 일기를 삭제하시겠습니까?`)) {
      onDelete(id);
    }
  };
  
  👉🏻const handleQuitEdit = () => {
    setIsEdit(false);
    setLocalContent(content);
  };

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

    if (window.confirm(`${id + 1}번쨰 일기를 수정하시겠습니까?`)) {
      onEdit(id, localContent);
      toggleIsEdit();
    }
  };

  return (
    <div className="DiaryItem">
      <div className="info">
        <span>
          작 성 자 : {author} | 감 정 점 수 : {emotion}
        </span>
        <br />
        <span className="date">
          시 간 : {new Date(created_date).toLocaleString()}
        </span>
        <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={toggleIsEdit}>수 정 하 기</button>
            <button onClick={handleRemove}>삭 제 하 기</button>
          </>
        )}
      </div>
    </div>
  );
};

export default DiaryItem;

<코드설명>

1) const DiaryItem = ({onEdit, onDelete, author, content, created_date, emotion, id})
= 부모인 DiaryList로부터 받은 onEdit함수를 prop으로 전달받은 후 호출하여 사용

1. 수정하기 버튼의 기능

1) const [isEdit, setIsEdit] = useState(false);
= useState에 기본값으로 false를 넣음 ➡️ isEdit의 역할은 true,false불리언 값을 갖게 되는데, 수정중인지 수정중이아닌지의 상태를 불리언값으로 나타낸다.
isEdit이 true가 되면 밑에 JSX에서 수정중으로 간주하여 수정취소,수정완료버튼에 대한 코드를 작성하면되고, false로 수정중이 아니라면 수정하기,삭제하기버튼이 된다.
즉, 수정중인지의 여부를 나타내는 코드!

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

= 원래 수정,삭제 버튼만 있었던곳에 삼항연산자를 이용하여 수정중이라면 수정취소,수정완료버튼을 / 수정중이 아니라면 수정,삭제 버튼으로 넣어주는 코드이다.

3)const toggleIsEdit = () => setIsEdit(!isEdit);
= toggleIsEdit함수는 호출이 되면 setIsEdit이 호출되면서 not연산을 통하여 원래 isEdit이 갖고있던 값을 반전시킨다. 이것은 수정하기버튼을 클릭하면 발생하는 함수!!

3){isEdit ? (
<>
<textarea
ref={localContentInput}
value={localContent}
onChange={(e) => setLocalContent(e.target.value)}
/>
</>
) : (
<>{content}</>
)}

= 원래는 {content}만 있었던곳에 isEdit과 삼항연산자를 이용하여
true이면 textarea를 localContent를 가져오고 이벤트(수정)가 일어나면 setLocalContent에 수정된내용을 전달해줘서 localContent를 업데이트해준다.
false이면 기존대로 {content}를 보여준다.

4)const [localContent, setLocalContent] = useState(content);
= localContent의 useState 초기값을 content로 바꿔주면, 수정하기 버튼을 클릭했을때 작성했던 내용이 뜬다.
💥근데, 이것만있으면 수정하기 누르고 수정한 후 다시 수정취소를 누르면 수정하려했었던 내용이 다시 뜬다. 이것을 방지하기위해 handleQuitEdit이라는 함수를 만든다. (말 보단 밑에 오류사진을 참고하면 좋을거같다.)




다시 수정하기 클릭하면 이전에 작성했던 내용이 남아있는 오류를 확인할 수 있다

2. 수정취소 버튼의 기능

1)const handleQuitEdit = () => {
setIsEdit(false);
setLocalContent(content);
};
= handleQuitEdit은 수정상태에서 나갈때 일어나는 함수
이 함수가 호출이 되면, setIsEdit이 false로 변하고(수정중이아님) setLocalContent로 localcontent값을 content로 만들어준다. (수정중인 폼은 localContent가 관리하기때문!)
이 함수는 수정취소버튼을 누를때 실행되도록 onClick이벤트에 넣어준다.

3. 수정완료 버튼의 기능

1)const localContentInput = useRef();
= localContentInput은 아래에 textarea를 focus할것이기때문에 밑에 태그가서 지정해줄것을 정의한다.

2){isEdit ? (
<>
<textarea
ref={localContentInput}
value={localContent}
onChange={(e) => setLocalContent(e.target.value)}
/>
</>
) : (
<>{content}</>
)}

= textarea태그에 ref기능을 넣어준다.

2)const handleEdit = () => {
if (localContent.length < 5) {
localContentInput.current.focus();
return;
}
if (window.confirm(${id + 1}번쨰 일기를 수정하시겠습니까?)) {
onEdit(id, localContent);
toggleIsEdit();
}
};
= handleEdit함수는 ⭐️onEdit함수를 호출⭐️해야하고 매개변수로 id와 새로운내용인localContent를 전달해야하는데, 내용은 기존조건이었던 5글자이상이 작성되도록 if문으로 조건을 걸어두자
focus기능을 주려면 DOM요소를 지정해야하기때문에 ref기능이 필요하다.
(상단에 import { useRef, useState } from "react";작성을 잊지말것)

🥲나의 문제

localContent.current.focus();이렇게 쓸뻔했다.
잘 더더 생각해보면 localContent는 수정내용이고 그 내용을 담는 곳에 focus를 주어야하는 상황이니까 그것을 담는 textarea에 ref를 해줘야한다. 아차 싶었다 ㅠㅠ 하나하나 잘 생각하고 코딩을 해야한다는점 잊지말자!!!

🖥코드 완성화면


일기 작성완료상태

수정중인 상태

수정완료버튼을 클릭한 상태

수정완료된 상태

<맘에안드는 점은 수정시간이 바뀌지않는다..?>


🚀참고자료

React강의-이정환강사

profile
FrontEnd Developer (with 구글신)

2개의 댓글

comment-user-thumbnail
2022년 5월 25일

github의 Diary_React_Project에서는 코드가 하나도 안 보입니다ㅠㅠ

1개의 답글