CRUD에 관한 간단한 설명과, 그 중 하나인 Create 기능의 React로의 구현을 4장에서 다룬다.

CRUD란?

CRUD란 대부분의 컴퓨터 소프트웨어가 가지는 기본적인 데이터 처리 기능인 Create(생성), Read(읽기), Update(갱신), Delete(삭제)를 묶어서 일컫는 말로, 사용자 인터페이스가 갖추어야 할 기능(정보의 참조/검색/갱신)을 가리키는 용어로서도 사용된다.

이미 CRUD를 안다면 웬 뜬금없는 CRUD인가 할 수 있지만 우리가 지금껏 구현해 온 기능들은 모두 CRUD 중 Read(읽기)에 속한다. 유저에게 그들이 읽을 수 있는 정보를 보여주는 방법을 배운 것이다.
이제 CRUD 중 C, U, D를 알아볼 차례다.

Create 기능 구현

3장의 정리까지는 (생활코딩) egoing님의 코드에서 일부분을 수정해 필자의 편의에 따라 정리하였으나, 4장의 CUD 기능구현에서는 본 정리가 참조한 강의의 코드를 큰 변경 없이 사용할 것이다. 아래는 CUD 구현 전의 egoing님의 코드를 필자가 일정 부분 리팩토링한 것이다.

(변경내역: for문에서 map() or forEach(), 들여쓰기, 변수 이름 변경, 세미콜론 사용 등)
(출처 : 생활코딩 - React 강의, 'state', egoing, https://opentutorials.org/course/4900/31268)

이 코드의 실행 뷰(미리보기)는 이렇게 보여진다.

요구사항

다음의 요구사항에 맞게 코드를 수정한다고 해 보자.

  1. 뷰의 최하단에 'create'라는 a 태그가 있고, 이를 클릭하면 세개의 <li> 아래의 본문인 article 내부가 다음과 같이 변경된다.
    <h2>Create</h2>
    <form>
    <p><input type="text" name="title" placeholder="title"/></p>
    <p><textarea name="body" placeholder="body"></textarea></p>
    <p><input type="submit" value="Create"></input></p>
    </form>
  2. 만약 유저가 (1)의 입력창에 title, body를 입력하고 Create 버튼을 클릭하면, 상단 <li>에 입력 title이 추가된다. 그리고 입력창과 버튼이 사라지고, 방금 작성한 title, body가 표시된다.
  3. (2)에서 생성된 title을 클릭하면, (1, 2, 3번 <li>와 같은 형식으로) (2)에서 입력했던 title, body가 본문에 출력된다.

이제 직접 코드를 수정해 보자.

구현

우선 처음으로, 요구사항 1번을 먼저 구현하자. 요구사항 1번을 구현하려면 우선 유저가 클릭할 a 태그가 필요하다. 이를 App()에 작성하자.

React에서는 실제 페이지 이동을 하지 않고 컴포넌트를 이용, 웹페이지의 내용을 수정하는 것이 핵심이므로, 이 태그를 클릭하면 mode State가 'CREATE' 모드로 변경되도록 하겠다. 또 새로고침도 막자.

이제 a 태그를 클릭하여 mode를 'CREATE'로 바꿀 수 있게 되었다. 이는 State를 변경하는 것이므로, React 기초 정리 3장에서 배웠듯 mode 변경 시 App()을 재실행한다.
이제 App()이 재실행 될 때, mode의 값에 따라 content(본문 내용을 표시하는 변수, 75번 줄)의 값을 바꾸도록, App() 내부의 if문을 수정해야 한다.
Content의 값을 직접 App() 내부에 적지 않고, Create 컴포넌트를 제작할 것이므로, 코드를 다음과 같이 추가했다.

이제 Create 컴포넌트를 App() 바로 위에 만들어 주자. 내용은 요구사항 1번에 설명되어 있다.

Create 컴포넌트를 작성했으니, 이를 보여주는 App()의 if문 내부에 컴포넌트의 주 행동을 onCreate라는 함수로 작성해 주자. 이 함수는 Create 컴포넌트로 입력된, 유저의 title과 body 입력값을 사용하게 될 것이다.

방금 생성한 onCreate의 콜백 함수에 유저의 입력값을 넘겨주려면, Create 컴포넌트에서 submit 시 title, body의 값을 가져와야 한다. <form>onSubmit 속성을 이용해 데이터를 전달하자. 새로고침도 방지하자.

이와 같이 코드를 작성하면 event.targetform 태그를 가리키므로, 내부 title과 body의 정보를 파라미터로 넘기고 onCreate의 콜백 함수를 실행시키게 된다.
onCreate의 콜백 함수는 title, body를 유저에게 입력받고, submit 버튼을 눌렀을 때 실행된다. 그렇다면 이 함수 내부에서는, 뷰 상단의 Navform에 입력된 정보를 바탕으로 한 <li>를 생성하고, 배열 topics에 입력된 정보를 object로 추가해야 한다.
topics가 변경될 때, 화면을 재렌더링 해야 하므로, topics를 State로 변경한다.

그리고 아래 if문에서 mode가 'CREATE'로 바뀔 경우, onSubmit 시 실행될 onCreate 콜백 함수 내부에 topics에 추가할, newTopic object 변수를 생성한다.

여기서 우리는 topics의 원소 object에, id 값이 존재하지만 위 코드에는 없는 것을 볼 수 있다.
새롭게 id를 관리할 State를 생성하고, 이를 사용해 id를 할당하자.

이제 새로 생성된 object인 newTopictopics 배열에 추가해야 한다. 여기서 주의할 점은, 배열 topics를 State로 선언하였기 때문에 기존 Vanilla JS의 push 메소드를 사용하면 안 된다.
State로 선언한 객체의 값을 변경(혹은 추가, 삭제)하기 위해서는, 다음과 같은 특수한 방법이 사용된다.

State로 선언된 객체의 변경

쉬운 이해를 위해, 객체(배열, Array) State인 fruits를 생성해 보자.

const [fruits, setFruits] = useState(['apple', 'banana', 'grape']);

만약 fruits에 'mango'를 추가하고 싶다면, 이렇게 해야 한다고 생각할지도 모른다.

fruits.push('mango');
setFruits(fruits);

하지만 이렇게 setFruits를 사용하면, State는 App()을 재렌더하지 않는다.
그 이유는 setState에 사용한 객체 자체가, 기존 fruits와 다른 객체가 아니기 때문이다.
만약 fruits의 값을 변경하고 싶다면, 다음과 같이 fruits의 복사본을 생성해 setState의 파라미터로 입력해 주어야 한다. 여기서는 ES6에서 추가된 문법인 Spread Operator을 사용해 fruits의 얕은 복사본(Shallow copy)를 만들어, State를 수정했다.

const newFruits = [...fruits];
newFruits.push('mango');
setState(newFruits);

이렇게 복사본을 수정 후 setState에 입력하면, 원하는 결과를 얻을 수 있다.




그렇다면, 계속해서 수정해 보자. 이제 topics를 복사한 newTopics 변수를 생성, 수정한 뒤 State를 변경하면 된다.

이와 같이 코드를 작성하면 잘 작동한다. 이제 요구사항 (2)의 마지막 문장대로, 입력 시 방금 입력한 title, body가 SUB모드에서 보이도록 수정하자. 또, id를 지정하고, 다음 생성될 글을 위해 nextId를 변경하자(1을 추가).

미리보기를 작동시켜 보면, 요구사항을 만족시키며 잘 작동한다.
아래는 최종 완성된 코드 전문이다.

import './App.css';
import {useState} from 'react';

function Article(props){
  return (
    <article>
      <h2>{props.title}</h2>
      {props.body}
    </article>
  );
}

function Header(props){
  return (
    <header>
      <h1><a href="/" onClick={(event)=>{
        event.preventDefault();
        props.onChangeMode();
      }}>{props.title}</a></h1>
    </header>
  );
}

function Nav(props){
  const lis = props.topics.map((topic) => {
    return (
      <li key={topic.id}>
        <a id={topic.id} href={'/read/' + topic.id} onClick={event => {
          event.preventDefault();
          props.onChangeMode(Number(event.target.id));
        }}>{topic.title}</a>
      </li>
    );
  });
  return (
    <nav>
      <ol>
        {lis}
      </ol>
    </nav>
  );
}

function Create(props) {
  return (
    <article>
      <h2>Create</h2>
      <form onSubmit={(event) => {
        event.preventDefault();
        const title = event.target.title.value;
        const body = event.target.body.value;
        props.onCreate(title, body);
      }}>
        <p><input type='text' name='title' placeholder='title'></input></p>
        <p><textarea name='body' placeholder='body'></textarea></p>
        <p><input type='submit' value='Create'></input></p>
      </form>
    </article>
  );
}

function App() {
  const [mode, setMode] = useState('MAIN');
  const [id, setId] = useState(null);
  const [nextId, setNextID] = useState(4);
  const [topics, setTopics] = useState([
    {id:1, title:'html', body:'html is ...'},
    {id:2, title:'css', body:'css is ...'},
    {id:3, title:'javascript', body:'javascript is ...'}
  ]);
  let content = null;

  if (mode === 'MAIN') {
    content = <Article title="Welcome" body="Hello, WEB"></Article>;
  } else if (mode === 'SUB') {
    let title, body = null;
    topics.forEach((topic)=> {
      if (topic.id === id) {
        title = topic.title;
        body = topic.body;
      }
    });
    content = <Article title={title} body={body}></Article>;
  } else if (mode === 'CREATE') {
    content = <Create onCreate={(title, body) => {
      const newTopic = {id: nextId, title: title, body: body};
      const newTopics = [...topics];
      newTopics.push(newTopic);
      setTopics(newTopics);
      setMode('SUB');
      setId(nextId);
      setNextID(nextId + 1);
    }}></Create>;
  }

  return (
    <div>
      <Header title="WEB" onChangeMode={()=>{
        setMode('MAIN');
      }}></Header>
      <Nav topics={topics} onChangeMode={(selectedID)=>{
        setMode('SUB');
        setId(selectedID);
      }}></Nav>
      { content }
      <a href='/create' onClick={(event) => {
        event.preventDefault();
        setMode('CREATE');
      }}>Create</a>
    </div>
  );
}

export default App;

여기까지가 Create 기능의 완성이다. State를 사용해 새 항목을 생성하고, 뷰를 재렌더했다.
다음 정리 5강에서는 Update 기능을 제작해 보겠다.