세현's 개발로그

[React] React에서 배열 사용하기 (feat.일기장) 본문

React

[React] React에서 배열 사용하기 (feat.일기장)

SarahPark 2023. 4. 30. 01:39

 

◈  리스트 렌더링(조회)

배열을 사용하여 React에서 List를 랜더링 하고, 개별적인 컴포넌트로 만들어보자.

App.js

우선, dummyList를 만들어준다. new Date() 객체를 생성하면 시간은 현재 시간을 기준으로 생성 된다. 이때 getTime이라는 메소드를 이용하면 milliseconds로 표현해준다. 그리고 추후에 만들 DiaryList에 dummyList를 props로 전달해준다.

import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";

const dummyList = [
  {
    id: 1,
    author: "이정환",
    content: "하이1",
    emotion: 5,
    created_date: new Date().getTime()
  },
  {
    id: 2,
    author: "이정환",
    content: "하이2",
    emotion: 5,
    created_date: new Date().getTime()
  },
  {
    id: 3,
    author: "이정환",
    content: "하이3",
    emotion: 5,
    created_date: new Date().getTime()
  }
];

const App = () => {
  return (
    <div className="App">
      <DiaryEditor />
      <DiaryList diaryList={dummyList} />
    </div>
  );
};
export default App;

 

DiaryList.js

App.js에서 받은 diaryList를 props로 전달 받는다.

그런데 App.js에서 props값을 undefined로 전달했을 가능성이 있으므로 defaultProps를 사용해준다.

 

map을 이용하여 추후에 만들 DiaryItem 컴포넌트에 dairyList의 원소를 하나씩 전달해준다.

map에서는 반드시 고유한 key라는 prop을 받아야 한다. 그래서 diaryList에서 id를 이용하여 key를 주었다(*고유한 키 값이 없는 경우에는 map 내장함수의 콜백함수의 두 번째 인자에 idx를 넣어주는 방법도 있다).

마지막으로 스프레드 연산자를 이용하여 각 원소의 내용을 펼쳐준다.

import DiaryItem from "./DiaryItem";

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

DiaryList.defaultProps = {
  diaryList: []
};

export default DiaryList;

 

DiaryItem.js

DiaryList 컴포넌트에서는 리스트를 랜더링하는 기능만을 담당하기 위해 DiaryItem 컴포넌트를 새로 만들어준다.

시간을 표시해줄 때 toLocaleString 메소드를 이용하여 우리가 알아보기 쉬운 형태로 출력해주도록 한다.

const DiaryItem = ({ id, author, content, emotion, created_date }) => {
  return (
    <div className="DiaryItem">
      <div className="info">
        <span className="author_info">
          | 작성자 : {author} | 감정점수 : {emotion} |
        </span>
        <br />
        <span className="date">{new Date(created_date).toLocaleString()}</span>
      </div>
      <div className="content">{content}</div>
    </div>
  );
};

export default DiaryItem;

App.css

/* List */
.DiaryList {
  border: 1px solid gray;
  padding: 20px;
  margin-top: 20px;
}

.DiaryList h2 {
  text-align: center;
}

/* Item */
.DiaryItem {
  background-color: rgb(240, 240, 240);
  margin-bottom: 10px;
  padding: 20px;
}

.DiaryItem .info {
  border-bottom: 1px solid gray;
  padding-bottom: 10px;
  margin-bottom: 10px;
}

.DiaryItem .date {
  color: gray;
}

.DiaryItem .content {
  font-weight: bold;
  margin-bottom: 30px;
  margin-top: 30px;
}

현재까지의 모습

◈  State 끌어올리기

현재까지 우리가 작성한 코드를 트리구조로 생각해보면 <App/> 부모 컴포넌트 아래 <DiaryEditor/>, <DiaryList/> 자식 컴포넌트가 연결되어 있는 구조이다. 이때 이 두 자식 컴포넌트끼리는 같은 계층에 있다고 할 수 있다. React에서는 같은 계층끼리는 데이터를 주고 받을 수 없다. DiaryEditor의 데이터를 DiaryList로 전달하기 위해서는 어떻게 해야할까?

 

이때, React의 state를 자식 컴포넌트의 부모 컴포넌트로 끌어올려서 해결할 수 있다.

<App/>가 [data, setData]를 가지고 있다면 <DiaryEditor/>에는 setData를, <DiaryList/>에는 data를 prop으로 전달해준다. 

이때 <DiaryEditor/>에서 새로운 일기를 작성하여 create라는 이벤트가 발생하여 setData가 호출되면 state가 랜더링 되어 추가 데이터가 <DiaryList/>에 전달 되는 것이다.

정리하자면, React에서는 event는 위에서 아래로 흐르는 역방향 이벤트 흐름, data는 위에서 아래로 흐르는 단방향 데이터 흐름을 가지고 있다.

 

이를 바탕으로 DiaryEditor.js에서 새로운 일기가 작성되면 상태변화함수가 부모컴포넌트로 전달되어 새로운 state를 DiaryList.js에 추가되어 랜더링 되도록 코드를 작성해보자.

App.js

먼저, 이전에 만들어줬던 dummyList를 지워주었다.

const [data, setData] = useState([ ]); 로 일기 배열을 초기화해준다.

그리고 const dataId = userRef(0);로 배열에 고유한 id를 부여해주기 위해 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]);
};

일기장에 새로운 배열을 추가해주기 위한 onCreate 함수를 만들어주었다.

이때 onCreate함수는 매개변수로 author, content, emotion을 받고 newItem 변수에 새로 추가될 값들을 모두 묶어서 저장해준다. dataId의 값은 매번 늘어나야 하므로 +1을 수행해준다. 그리고 setData에 newItem과 기존의 데이터 전부를 순서대로 전달해준다. 마지막으로 <DiaryEditor/>와 <DiaryList/>에 prop으로 각각 onCreate와 diaryList를 전달해준다.

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]);
  };

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

DiaryEditor.js

App.js에서 전달한 onCreate를 prop으로 받는다.

handleSubmit 함수에서 prop으로 받은 onCreate를 호출하고, 작성폼에서 작성한 내용들(state.author, state.content, state.emotion)을 넣어준다. 저장이 되면, setState를 이용하여 다시 상태를 초기값으로 돌려준다.

import { useRef, useState } from "react";

const DiaryEditor = ({ onCreate }) => {
  const authorInput = useRef();
  const contentInput = useRef();

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

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

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

    if (state.content.length < 5) {
      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}
          value={state.author}
          onChange={handleChangeState}
          name="author"
          placeholder="작성자"
          type="text"
        />
      </div>
      <div>
        <textarea
          ref={contentInput}
          value={state.content}
          onChange={handleChangeState}
          name="content"
          placeholder="일기"
          type="text"
        />
      </div>
      <div>
        <span>오늘의 감정점수 : </span>
        <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;

 

◈  배열 삭제하기

일기 배열에 삭제 버튼을 생성하고, 버튼을 누르면 배열이 삭제되도록 구현해보자.

App.js

삭제를 위한 onRemove 함수를 만들어준다.

targetId를 받아서 targetId를 가진 해당 요소를 제외한 배열을 만들어준다. 이때 filter() 기능을 사용해주었다. 삭제한 배열을 setData에 넣어주어 랜더링 해준다. 마지막으로 DiaryList에 prop으로 onRemove를 전달해준다.

function 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 onRemove = (targetId) => {
    const newDiaryList = data.filter(
      (it) => it.id !== targetId
    );
    setData(newDiaryList);
  };

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

DiaryList.js

App.js에서 받은 onRemove를 추가해준다. 그리고 onRemove를 자식 컴포넌트인 DiaryItem으로 내려준다. 사실상 DiaryList에서는 onRemove를 사용하지 않지만, DiaryItem에 있는 삭제 버튼이 App의 onRemove를 호출할 수 있어야 하므로 DiaryList를 통해서 DiaryItem에 onRemove를 전달한다. 이를 props drilling이라고 한다.

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

DiaryList.defaultProps = {
  diaryList: []
};

DiaryItem.js

전달받은 onRemove를 추가해준다. 그리고 삭제버튼을 생성하고, onClick 이벤트로 confirm창을 띄워 확인 버튼을 누를 경우 onRemove함수에 해당 id를 전달하여 삭제를 진행한다.

const DiaryItem = ({onRemove, id, author, content, emotion, created_date}) => {
  return (
    <div className="DiaryItem">
      <div className="info">
        <span className="author_info">
          작성자 : {author} | 감정점수 : {emotion}
        </span>
        <br />
        <span className="date">{new Date(created_date).toLocaleString()}</span>
      </div>
      <div className="content">{content}</div>
      <button
        onClick={() => {
          if (window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)) {
            onRemove(id);
          }
        }}
      >
        삭제하기
      </button>
    </div>
  );
};

◈  내용 수정하기

일기 배열에 수정하기 버튼을 생성해주고, 내용을 수정한 것이 반영되도록 구현해보자.

App.js

최종적으로 일기 배열을 수정하는 부분은 Diaryitem 컴포넌트이지만, 이벤트는 아래에서 위로 흐르는 것을 생각하여 부모 컴포넌트인 App.js에 이벤트가 일어나는 함수를 만들어줘야 한다. 이 함수를 onEdit이라고 하자.

onEdit 함수는 targetId와 newContent를 받아서 setData를 해준다. 이때 targetId와 일치하는 것의 내용은 수정해주고, 일치하지 않는 것의 내용은 원래의 내용을 유지한다.

마지막으로 DiaryList에 prop으로 onEdit을 전달한다.

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

 

DiaryList.js

부모 컴포넌트인 App으로부터 onEdit을 prop으로 전달 받고 자식 컴포넌트인 DiaryItem으로 prop을 전달한다.

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

DiaryList.defaultProps = {
  diaryList: []
};

DiaryItem.js

우선 부모로부터 onEdit을 prop으로 전달받는다.

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

useState에 기본값으로 false를 넣어준다. isEdit의 역할은 불리언 값을 통해 수정 중인지, 아닌지의 상태를 판단한다.

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

isEdit값이 True라면 수정취소/수정완료 버튼을 넣어주고, False라면 수정하기/삭제하기 버튼을 넣어준다. 

이때 삼항 연산자를 사용하여 표현해주었다.

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

toggleIsEdit 함수는 not 연산자를 통해 기존의 isEdit 값을 반전시킨다. 이는 수정하기 버튼을 클릭할 때 발생하는 함수다.

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

isEdit이 True라면 원래의 내용이 아닌, 새로 입력할 textarea를 띄워준다. 이때 원래의 내용인 localContent를 textarea안에 띄워준다. 내용이 입력되면서 이벤트가 발생하면(수정) setLocalContent을 통해 내용을 수정해준다. 

마찬가지로 내용이 5글자 미만으로 입력되면 focus가 되도록 ref 값을 지정해주었다.

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

수정하기 버튼을 클릭했을 때 원래의 내용을 띄워주기 위해 useState의 초기값을 content로 지정해준다.

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

수정하기를 누르고 수정한 후 수정취소를 누르고 다시 수정하기 버튼을 누르면 수정하려 했었던 내용이 다시 뜨는 문제가 발생한다. 이를 위해 수정취소 버튼을 눌렀을 때 초기의 content값이 나타나도록 handleQuitEdit 함수를 만들어주었다.

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

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

수정하고자 하는 내용의 길이가 5글자가 미만이면 focus를 해주고 return 한다.

정상적으로 입력되었다면 확인 문구를 띄우고 onEdit함수에 id값과 수정된 내용을 전달하면서 함수를 호출한다.

마지막으로 isEdit값을 반전시켜 수정하기 상태에서 빠져나온다.

import { useRef, useState } from "react";

const DiaryItem = ({
  onRemove,
  onEdit,
  id,
  author,
  content,
  emotion,
  created_date
}) => {
  const localContentInput = useRef();
  const [localContent, setLocalContent] = useState(content);
  const [isEdit, setIsEdit] = useState(false);
  const toggleIsEdit = () => setIsEdit(!isEdit);

  const handleClickRemove = () => {
    if (window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)) {
      onRemove(id);
    }
  };

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

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

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

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

 

Comments