본문 바로가기

Tech/React

프론트 입문 리액트 삽질기 4 - 배열을 활용한 컴포넌트 생성

반응형

Array, Component 생성

지난번 글에서 컴포넌트를 동적으로 생성하지 못해 componentWillUnmount 훅을 만들지 못했습니다.

이제 배열을 통해 컴포넌트를 동적으로 생성, 업데이트, 그리고 제거해 보겠습니다.

 

이 글은 Veloport님의 블로그리액트 공식 문서를 참고하여 작성했습니다.

 

리스트와 Key – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

누구든지 하는 리액트 7편: 배열 다루기 (1) 생성과 렌더링 | VELOPERT.LOG

이 튜토리얼은 10편으로 이뤄진 시리즈입니다. 이전 / 다음 편을 확인하시려면 목차를 확인하세요. 이번에는 리액트 프로젝트에서 배열을 다루는 방법을 알아보겠습니다. 리액트에서는 배열을

velopert.com

 

리액트에서 배열을 다룰 때에는 배열을 직접 수정하는 함수를 사용해서는 안됩니다(자세한 이유는 아래에 첨부하였습니다.).

일단 다음과 같이 App 컴포넌트에서 CardForm을 생성하고 handleOnCreate 함수를 CardForm 컴포넌트로 전달하도록 작성합니다.

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

function App() {
  const [cardId, setCardId] = useState(0)
  const [cardList, setCardList] = useState([])

  const handleOnCreate = (cardInfo) => {
    setCardList(cardList.concat({ id: cardId, name: cardInfo.name, description: cardInfo.description }))
    setCardId(c => c + 1)
  }

  return (
    <Fragment>
      <h1>React Array Example</h1>
      <CardForm onCreate={(cardInfo) => handleOnCreate(cardInfo)} />
    </Fragment>
  );
}

export default App;

 

그리고 CardForm 컴포넌트를 다음과 같이 작성합니다.

import { useState } from 'react';

function CardForm({ onCreate = (v) => console.log(v) }) {
    const [cardName, setName] = useState("")
    const [description, setDescription] = useState("")

    const handleSubmit = (e) => {
        e.preventDefault()
        onCreate({ name: cardName, description: description })
        setName("")
        setDescription("")
    }

    return (
        <form onSubmit={handleSubmit} style={{
            padding: '8px',
            margin: '8px'
        }}>
            <input type="text" value={cardName} name="cardName" placeholder="name" onChange={e => setName(e.target.value)} />
            <input type="text" value={description} name="cardDescription" placeholder="description" onChange={e => setDescription(e.target.value)} />
            <button type="submit"> Add </button>
        </form>
    );
}

export default CardForm;

 

App 컴포넌트부터 한 줄 한 줄 살펴보겠습니다.

우선 Props와 State Hook은 지난번 글에서 다루었으니, 아래 글을 참조해 주시기 바랍니다.

 

프론트 입문 리액트 삽질기 2 - Props, State Hook

지난번 글에서 Create-React-App을 통해 App 이라는 컴포넌트를 만들어 줬습니다. 이제 이 컴포넌트에 입력 상자를 하나 두고, 버튼을 누르면 환영 팝업이 나타나는 페이지를 하나 구성하려고 합니다.

sean-ma.tistory.com

 

그 아래에 선언한 handleOnCreate 함수는 CardForm 컴포넌트에 넘겨 줄 함수로, cardInfo를 인자로 받습니다.

card Id State와 cardInfo 인자를 묶어 cardList 배열에 집어넣고 cardId의 값을 하나 증가시킵니다.

const handleOnCreate = (cardInfo) => {
  setCardList(cardList.concat({ id: cardId, name: cardInfo.name, description: cardInfo.description }))
  setCardId(c => c + 1)
}

 

이 때, 위에서도 언급했듯이 cardList 배열에 push 등의 메소드를 사용하여 배열을 직접 변경하면 안됩니다.

그 이유는 리액트 공식 문서에서도 확인할 수 있습니다.

 

State and Lifecycle – React

A JavaScript library for building user interfaces

ko.reactjs.org

this.state를 직접 변경하면 안 됩니다. 왜냐하면 이후 호출되는 setState()가 이전에 적용된 변경 사항을 덮어쓰기 때문입니다.
this.state를 불변적(Immutable)인 데이터로 취급하세요.

따라서 배열에는 concat, filter, map 등의 새로운 배열을 만들어 내는 메소드를 사용해야 합니다.

따라서 setCardList 함수에 cardList.concat(...) 을 전달해 줍니다.

 

return (
  <Fragment>
    <h1>React Array Example</h1>
    <CardForm onCreate={(cardInfo) => handleOnCreate(cardInfo)} />
  </Fragment>
);

 

그리고 CardForm 컴포넌트의 Props로 handleOnCreate 함수를 전달해 줍니다.

 

이제 CardForm 컴포넌트로 이동해서, Props.onCreate의 기본 생성자로 console.log를 받도록 작성하고 State Hook을 작성합니다.

function CardForm({ onCreate = (v) => console.log(v) }) {
    const [cardName, setName] = useState("")
    const [description, setDescription] = useState("")
//...
}

 

부모 컴포넌트로부터 넘어온 onCreate 함수를 호출하여 입력된 데이터를 부모 컴포넌트로 전달하도록 handleSubmit 함수를 작성합니다.

const handleSubmit = (e) => {
    e.preventDefault()
    onCreate({ name: cardName, description: description })
    setName("")
    setDescription("")
}

 

그리고 JSX는 다음과 같이 form으로 묶어주고, 버튼의 type을 submit으로 지정하여 form의 onSubmit에 위에서 작성한 handleSubmit 함수를 전달해 줍니다.

이 때 form 상에서 onSubmit 이 발생하는데, 해당 동작 시에 기본적으로 페이지를 새로고침하기 때문에 handleSubmit 함수에 e.preventDefault()를 추가해 새로고침을 방지해 줍니다.

return (
    <form onSubmit={handleSubmit} style={{
        padding: '8px',
        margin: '8px'
    }}>
        <input type="text" value={cardName} name="cardName" placeholder="name" onChange={e => setName(e.target.value)} />
        <input type="text" value={description} name="cardDescription" placeholder="description" onChange={e => setDescription(e.target.value)} />
        <button type="submit"> Add </button>
    </form>
);

 

이제 App 컴포넌트에 Effect Hook를 추가해 정상적으로 동작하는지 확인해 줍니다.

useEffect(() => {
    console.log(cardList)
  }, [cardList])

 

Effect Hook을 추가하면 다음과 같이 동작하는 것을 확인할 수 있습니다.

App, CardForm 컴포넌트 동작 확인

Add 버튼을 클릭할 때 마다 콘솔에 찍히는 로그를 확인할 수 있습니다.

 

이제 이 배열을 콘솔이 아닌 컴포넌트로 화면에 나타내보려 합니다. 

일단 이 card 정보를 담을 CardInfo 컴포넌트를 생성합니다.

import { useEffect } from "react";

function CardInfo({ info = {name: "N/A", description: "N/A" } }) {

    useEffect(() => {
        console.log("CardInfo component created")
    }, [])

    return (
        <div style={{
            border: '1px solid black',
            padding: '8px',
            margin: '8px'
        }}>
            <div><b>{info.name}</b></div>
            <div>{info.description}</div>
        </div>
    );
}

export default CardInfo

 

default 생성자를 가진 CardInfo를 만들었고, 내부의 Effect Hook은 컴포넌트 생성 이벤트를 확인하기 위해 만들어 두었습니다.

이제 이 CardInfo를 배열 형태로 띄워 줄 CardInfoListView 컴포넌트를 생성해 봅니다.

import CardInfo from "./CardInfo";

function CardInfoListView({ cardList = [] }) {
    return (
        <div>
            {cardList.map(card => (<CardInfo key={card.id} info={card} />))}
        </div>
    );
}

export default CardInfoListView

 

한 줄씩 살펴보면, 디폴트 생성자로 빈 배열을 받는 컴포넌트를 생성하고, cardList에 map 메소드를 적용해 배열의 각 원소를 CardInfo로 변환시킨 새 배열을 보여주도록 작성했습니다.

map 메서드란 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환하는 함수입니다.

 

Array.prototype.map() - JavaScript | MDN

The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.

developer.mozilla.org

cardList.map(card => (<CardInfo key={card.id} info={card} />))

 

따라서 위 라인을 풀어보면, cardList의 각 원소들을 CardInfo 컴포넌트로 변환하는 동작을 수행하는 것을 알 수 있습니다.

이제 App 컴포넌트를 다음과 같이 수정해 줍니다.

import { Fragment, useEffect, useState } from 'react';
import './App.css';
import CardForm from './CardForm';
import CardInfoListView from './CardInfoListView';

function App() {
  const [cardId, setCardId] = useState(0)
  const [cardList, setCardList] = useState([])

  useEffect(() => {
    console.log(cardList)
  }, [cardList])

  const handleOnCreate = (cardInfo) => {
    setCardList(cardList.concat({ id: cardId, name: cardInfo.name, description: cardInfo.description }))
    setCardId(c => c + 1)
  }

  return (
    <Fragment>
      <h1>React Array Example</h1>
      <CardForm onCreate={(cardInfo) => handleOnCreate(cardInfo)} />
      <CardInfoListView cardList={cardList} />
    </Fragment>
  );
}

export default App;

 

위와 같이 CardInfoListView에 cardList state를 props로 전달해 줍니다.

Array.map을 통한 컴포넌트 생성

반응형