본문 바로가기
  • 기록
React/(Udemy)React-The Complete Guide

[React] 리액트 state, event 처리(섹션4)

by juserh 2022. 5. 16.

1. 이벤트 리스닝, 이벤트 핸들러 수행

현재는 위 모습의 state 하나밖에 존재하지 않는다. 

 

ExpenseItem컴포넌트에 이벤트 리스너 실습을 위한 버튼을 달아보았다.

import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "../UI/Card";

const ExpenseItem = (props) => {
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
      <button
        onClick={() => {
          console.log("clicked!");
        }}
      >
        Change Title
      </button>
    </Card>
  );
};

export default ExpenseItem;

<button>요소에 props로 on이벤트리스너(여기선 onClick으로 클릭이벤트 발생 시 실행)를 달았다. 버튼을 클릭하면 콘솔창에 "clicked!" 메시지가 뜨는 것을 확인할 수 있다.

위 코드에서는 이벤트 발생 시 실행될 코드를 JSX코드에 넣었는데 따로 함수를 정의하여 넣는 것이 보기 깔끔하고 좋다.

import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "../UI/Card";

const ExpenseItem = (props) => {
  const clickHandler = () => {
    console.log("Clicked!!");
  };
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
      <button onClick={clickHandler}>Change Title</button>
    </Card>
  );
};

export default ExpenseItem;

이렇게!! constant가 말고 function으로 정의해도 상관없음.

 

여기서 주의할 점...!

<button onClick={clickHandler()}>Change Title</button>

여기에 이렇게 괄호를 추가하면 안된다. 괄호를 추가할 경우 클릭 이벤트가 발생했을 때 clickHandler가 발생하는 것이 아니라, JSX코드가 평가될 때 clickHandler가 실행된다. 실제로 실행을 해보니 괄호를 넣은 경우에, 새로고침 되고 바로 콘솔 로그 4개가 찍혀있었고 클릭했을 때는 로그가 달리지 않았다. 그래서 이벤트 핸들러를 리스너에 달 때 괄호를 빼고 함수만 지정해야한다.

 


2. 컴포넌트 기능 실행 방법

이번엔 이벤트가 발생했을 때 화면에서 변화가 일어나도록 해보자. 클릭 이벤트가 발생했을 때 title을 업데이트하도록 바꿔보려고 한다.

ExpenseItem에 let으로 title을 담을 변수를 만들어준다. 그리고 clickHandler 내용도 바꾸어주었다.

import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "../UI/Card";

const ExpenseItem = (props) => {
  let title = props.title;

  const clickHandler = () => {
    title = "Updated!";
    console.log(title);
  };
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
      <button onClick={clickHandler}>Change Title</button>
    </Card>
  );
};

export default ExpenseItem;

이렇게 하면 될 것 같지만....어째서인지 안된다..! 콘솔창에는 업데이트된 타이틀이 뜨지만 화면에서는 변화를 확인할 수 없었다.

 

왜 DOM에서는 변화가 나타나지 않는 것일까? 리액트는 index.js의 App컴포넌트를 시작으로 존재하는 모든 컴포넌트 함수들을 평가하고 실행하여 브라우저에 나타낸다. 여기서 문제는 리액트는 이 과정을 반복하지 않는다는 것이다. 리액트는 처음 렌더링될 때 위 과정을 실행하고 끝낸다. 그래서 우리는 리액트에게 어떤 변화가 일어났음을 알리고 특정 컴포넌트가 다시 확인되어야 함을 알려야한다. 그 과정이 없었기 때문에 위 코드로는 화면에서 변화를 확인할 수 없었던 것이다. 그리고 그 과정을 거쳐 변화를 만들어내기 위해서는 State의 개념을 알아야한다.

 


3. State

ExpenseItem 컴포넌트 함수 코드는 처음 렌더링에 실행된 이후, 다시 재실행되지 않아 값이 변화는 것을 보여줄 수 없다. 

특정 부분을 다시 실행하기 위해서는 리액트 라이브러리에서 임포트해야할 것이 있다.

import React, { useState } from 'react';

useState은 특정 value를 state이라는 이름으로 찾고 이 value가 바뀌면 컴포넌트(의 반환문html코드)에 반영되도록 한다. 중요한 리액트 훅 중의 하나이다. 리액트 훅은 반드시 컴포넌트 함수 내에서 호출되어야 하고 중첩된 함수(컴포넌트 내에 포함되는 또 다른 함수)에는 호출할 수 없다.

import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "../UI/Card";
import { useState } from "react";

const ExpenseItem = (props) => {
  const [title, setTitle] = useState(props.title);

  const clickHandler = () => {
    setTitle("Updated!");
    console.log(title);
  };
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
      <button onClick={clickHandler}>Change Title</button>
    </Card>
  );
};

export default ExpenseItem;

useState 함수는 변수와 변수 값을 변환하는 함수를 배열로 반환한다. 반환된 배열의 두번째 요소에 해당하는 변수 값 재설정 함수를 이용해 값을 업데이트하면 리액트는 해당 useState가 포함된 특정 컴포넌트를 재실행하여 변환된 것이 화면에 보이게 된다.

변환 성공!

브라우저 개발자 모드 콘솔을 보면 로그는 변화되기 전 값으로 찍혀있는데, 이는 setTitle로 값을 변환하긴하지만 곧바로 변환되는 것이 아니기 때문에 변환 이전의 초기값이 로그에 찍히는 것이라고 한다.

 


4. useState 훅

같은 컴포넌트이더라도 각 인스턴스마다 별도의 state을 갖는다는 것이 중요!!

//Expenses.js
import React from "react";
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";
import Card from "../UI/Card";

const Expenses = (props) => {
  return (
    <Card className="expenses">
      <ExpenseItem
        title={props.items[0].title}
        amount={props.items[0].amount}
        date={props.items[0].date}
      />
      <ExpenseItem
        title={props.items[1].title}
        amount={props.items[1].amount}
        date={props.items[1].date}
      />
      <ExpenseItem
        title={props.items[2].title}
        amount={props.items[2].amount}
        date={props.items[2].date}
      />
      <ExpenseItem
        title={props.items[3].title}
        amount={props.items[3].amount}
        date={props.items[3].date}
      />
    </Card>
  );
};

export default Expenses;

Expenses.js 파일을 살펴보면 ExpenseItem 컴포넌트를 4개를 불러왔다. 

지금까지의 작성한 코드를 실행해보면, 각 아이템의 changeTitle버튼을 클릭했을 때 해당 아이템의 title만 변경되고 다른 아이템은 영향을 받지 않는다는 것을 확인할 수 있다.

->즉, state은 인스턴스마다 별도로 생성된다!!

 

//ExpensItem.js
import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "../UI/Card";
import { useState } from "react";

const ExpenseItem = (props) => {
  const [title, setTitle] = useState(props.title);
  console.log("ExpensesItem evaluated by React"); //코드 추가

  const clickHandler = () => {
    setTitle("Updated!");
    console.log(title);
  };
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{title}</h2>
        <div className="expense-item__price">${props.amount}</div>
      </div>
      <button onClick={clickHandler}>Change Title</button>
    </Card>
  );
};

export default ExpenseItem;

ExpenseItem 컴포넌트에 console.log()코드를 추가하여서, 해당 컴포넌트가 평가되고 실행될 때 콘솔에 로그가 찍힌다. 브라우저를 새로고침하면,

이렇게 모든 ExpenseItem 컴포넌트의 인스턴스에서 해당 로그가 찍히는 것을 확인할 수 있다.

후에, 특정 아이템의 changeTitle 버튼을 누르면,

위와 같이 해당하는 인스턴스에 대해서만 컴포넌트가 재평가, 실행되는 것을 확인할 수 있다. 그리고 재실행되면 우리는 가장 최신의 state을 받아볼 수 있다.

 

useState은 결과로 두 값을 담은 배열을 제공한다.
배열의 첫번째는 현재 상태, 두번째는 상태변환 함수(set-)이다.
상태변환 함수로 state을 바꿀 수 있고, 이 함수가 호출되면 해당 컴포넌트가 재실행되며 state 값을 업데이트한다.

5. 입력 양식 추가

사용자가 비용정보를 입력하는 입력창을 만드려고 한다. NewExpense.js와 ExpenseForm.js 2개의 파일로 구성할 것이다. NewExpense컴포넌트는 특별한 것 없이 <div>태그로 ExpenseForm 컴포넌트를 감싸는 구성만 가진다. ExpenseForm ㅓ컴포넌트는 <input>태그로 사용자에게서 title, amount, date 등을 입력받는다.

//NewExpense.js
import React from "react";
import "./NewExpense.css";
import ExpenseForm from "./ExpenseForm";

const NewExpense = () => {
  return (
    <div className="new-expense">
      <ExpenseForm />
    </div>
  );
};

export default NewExpense;

 

//ExpenseForm.js
import React from "react";
import "./ExpenseForm.css";

const ExpenseForm = () => {
  return (
    <form>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input type="text" />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input type="number" min="0.01" step="0.01" />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input type="date" min="2019-01-01" max="2022-12-31" />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

그리고 NewExpense 컴포넌트를 App.js에서 임포트하여 사용하면,

이와 같이 브라우저가 구성된 것을 볼 수 있다.

 


6. 사용자 입력 리스닝

위에서 입력 폼을 만들어 주었으니, 이제 이벤트 리스너를 달아서 입력 후 버튼을 누르면 해당 정보를 가져와야 하는데...어떻게 해야할까?

 

ExpenseForm.js에서 title <input>에 onChange 리스너를 달아 무언가 변경이 일어날 때마다 특정 코드가 실행되도록 할 수 있다.

  const titleChangeHandler = (event) => {
    console.log(event);
  };

위 핸들러를 작성하여 onChange 리스너에 달았다. 위 코드는 event 객체를 이용해서 콘솔 창에 발생한 event 객체를 출력해준다.

event객체

title 입력창에 아무 내용을 작성하면 위와 같은 event 객체를 확인할 수 있다. 이 중에서도 target속성을 자세히 보면,

event객체-target속성-value

target속성의 value가 해당 input에 작성된 내용을 담고 있는 것을 볼 수 있다. target속성은 event가 발생한 DOM요소를 가르킨다. 때문에 'target: input'라고 되어있는 것이다(title <input>태그에서 change가 발생한 것이므로). 또 target속성의 value는 event가 발생한 시점에서의 현재 입력값을 갖는다.

 

그럼 이번에 핸들러에 event.target.value를 이용해보자.

  const titleChangeHandler = (event) => {
    console.log(event.target.value);
  };

그리고 콘솔창을 확인하면,

위와 같이 입력할 때마다 그때 input태그의 현재 입력값이 출력되는 것을 볼 수 있다.

 


7. 여러 State 다루기

그럼 이제 <input>에서 바뀌는 입력값을 어딘가에 저장해주어야 할텐데..state을 이용하자(다른 방법도 있는데, 일단 state으로!)

  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");
  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };
  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };
  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

title 외에도 amount, date <input>에 대해서도 state과 handler를 작성에 onChange속성에 이용했다. 위와 같이 작성하면 이제 event가 발생하면 각 <input>태그의 입력값이 각각의 state에 저장될 것이다.

 


8. State 대신 사용하기(더 나은 방법)

위 코드는 똑같은 개념, 과정(useState, handler)를 3번 반복한 것인데, 이를 한번의 state으로 구현할 수 있다.

바로 처음 useState()으로 선언을 할 때, 초기값에 객체를 넣어주는 것이다.

  const [userInput, setUserInput] = useState({
    enteredTitle: "",
    enteredAmount: "",
    enteredDate: "",
  });

이렇게 하면 한번에 state을 담을 수 있다.

const titleChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      endteredTitle: event.target.value, //override
    });
  };
  const amountChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      endteredAmount: event.target.value, //override
    });
  };
  const dateChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      endteredDate: event.target.value, //override
    });

그리고 handler도 위와 같이 수정했다. spread operater로 이전의 state 객체인 userInput을 가져와서, event가 발생한 각 input에 해당하는 state만 override해준다.

 

3개의 state을 별개로 선언하여 사용하는 방법과 3개의 state을 하나의 객체에 담아 사용하는 방법 2가지를 살펴봤는데 위 두 가지 방법 모두 사용해도 괜찮다. 그런데 두 번째 방법에서 주의해서 봐야할 것이 있는데, 바로 handler부분에 setUserInput()으로 state 객체를 새로 업데이트할 때 spead operater로 이전 state 객체를 이용해야한다는 것이다.

 


9. 이전 state에 의존하는 state 업데이트

  const titleChangeHandler = (event) => {
    setUserInput({
      ...userInput,
      endteredTitle: event.target.value, //override
    });
  };

위에서 작성한 handler 코드를 살펴보면 새롭게 state을 업데이트할 때 이전 state을 가져와 override하고 있는 것을 확인할 수 있다. 리액트의 state 업데이트는 곧바로 실행되는 것이 아니기 때문에 위와 같이 작성할 경우 문제가 생길 가능성이 있다(가장 최근 상태를 가져와 업데이트에 활용해야 하는데, 잘못된 시기의 state를 가져오는 등의 문제 등).

 

그래서 위와 같이 이전 상태에 의존해서 업데이트해야 하는 state에 대해서는 아래와 같이 코드를 작성해야 한다.

  const titleChangeHandler = (event) => {
    setUserInput((prevState) => {
      return { ...prevState, endteredTitle: event.target.value };
    });
  };

setUserInput()(즉, useState 훅으로 리턴된 state업데이트함수)안에 ()=>{} 함수를 넣으면 리액트가 알아서 이전 상태의 State(여기서는 prevState으로 이름 지음)의 스냅샷을 가져오고 이를 활용해 적절하게 state 리턴해주면 된다. 위와 같이 작성하면 리액트가 위 함수 폼을 인식하여 가장 최근 state을 올바르게 가져오는 것이 보장되어 반드시! 위와 같이 작성해야 한다.

 

이렇게 3개의 state을 하나의 객체에 담아 이용하고 state를 업데이트하는 방법까지 보았다.

그리고 나는 3개의 state을 별도의 state 3개로 선언하여 진행하겠다(이유는 그냥 강의에서 그렇게 해서..)

 


10. 양식 제출 처리

먼저 입력 폼에 적은 내용을 어떻게 보내는지 알아보자. 우리는 <form>태그 안에 <button>태그가 있고, 해당 <button>태그는 "submit" type 속성을 가진다. 그래서 <button>에 onClick 리스너를 달지 않고 폼의 내용을 전달할 수 있다. <form>태그에 onSubmit 리스너를 다는 것이다.

<form onSubmit={submitHandler}>

이렇게 submitHandler를 달았는데,

그렇다면 submitHandler에서는 어떤 일을 해야하나 생각해보자. 입력폼에 작성한 3개의 내용을 모두 가져와 보내야할 것이다. 그런데 지금 나는 지금 3개의 state을 각각 별도로 선언하여 사용하고 있기 때문에, 이를 하나의 객체로 합쳐줘야한다.

  const submitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };
    console.log(expenseData);
  };

그래서 위 코드를 보면 expensesData 객체에 3개의 state를 넣어주었다. 

event.preventeDefault()를 추가한 이유는 submit event가 실행되면 브라우저에서 자동으로 새로고침이 되어서 그 부분을 막기 위해서이다.

이렇게 handler를 작성하고 브라우저에서 입력폼 작성하고 버튼을 눌러 실행해보면,

이렇게 합쳐진 expenseData가 잘 출력되는 것을 볼 수 있다.

 


11. 양방향 바인딩(two way binding) 추가

먼저 two way binding이 뭔가..

https://stackoverflow.com/questions/13504906/what-is-two-way-binding

 

What is two way binding?

I have read lots that Backbone doesn't do two way binding but I don't exactly understand this concept. Could somebody give me an example of how two way binding works in an MVC codebase and how it ...

stackoverflow.com

  1. 모델의 프로퍼티가 업데이트되면, 그게 UI에 보여지고
  2. UI의 엘리먼트가 업데이트되면, 그 변화가 또 모델에 반영되는 것

즉, View(UI)와 Model(실제 데이터)가 서로가 서로에게 양방향으로 영향을 미치는 것을 말한다.

 

앞 내용까지의 코드를 브라우저에서 실행하면 입력폼에 내용을 입력하고 submit버튼을 누르면 콘솔창에 입력한 데이터를 출력된다. 그러나 그 후에 입력폼 값이 그대로 남아있게 되는데 이것을 two way binding으로 지워주려고 한다.

 

import React, { useState } from "react";
import "./ExpenseForm.css";

const ExpenseForm = () => {
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");

  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };
  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };
  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

  const submitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };
    console.log(expenseData);
    setEnteredTitle("");
    setEnteredAmount("");
    setEnteredDate("");
  };
  return (
    <form onSubmit={submitHandler}>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            value={enteredTitle}
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            value={enteredAmount}
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max="2022-12-31"
            value={enteredDate}
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

기존 코드에 추가한 것은 크게 2부분으로 볼 수 있다.

  • title, amount, date의 <input> 태그에 value속성을 추가하고 그 값으로 해당하는 state을 지정해주고,
  • Handler함수에 입력폼 데이터 수집 후, setState으로 모든 state 값을  ""(빈문자열)로 변경하였다.

이게 왜 two way binding 일까?

  • <input>태그에 넣은 각각의 stateChangeHadler: input에서 변화가 일어나면, 그 변화를 setState을 이용해 state에 반영됨(<input> -> state)
  • 제출을 위한 submitChangeHandler: setState("")으로 state 값을 변경해주면, 그 변화가 <input>태그의 입력내용에 반영됨(state -> <input>)

하여튼 이렇게 코드를 수정하고 실행해보면, 제출버튼을 누르면 입력내용이 콘솔창에 출력되고 입력폼 내용이 깨끗하게 지워진다.

 


12. 자식 대 부모 컴포넌트 통신(상향식)

이렇게 입력폼에 입력한 내용을 가져와 결합하여 data로 가지게 되었지만, 그것은 여전히 ExpenseForm.js 파일에 존재해 있고, 이 컴포넌트에 데이터가 존재하는 것은..사실..의미가 없다. 우리는 입력한 데이터를 또 다른 ExpenseItem으로 보여줘야 하므로, 이것을 expenses데이터 배열이 존재하는 App.js에 전달해야한다.

 

지금까지 우리는 부모에서 자식으로 향하는 데이터 전달만 했다(props 이용). 이번엔 자식에서 부모로 데이터를 전달해야하는데.. 어떻게 해야할까....? ....그런데 사실 우리는 이를 우리가 알게 모르게 하고 있었다고 한다...!

 

          <input
            type="text"
            value={enteredTitle}
            onChange={titleChangeHandler}
          />

위는 우리가 입력폼에 작성한 title <input>태그 부분이다. 위 코드를 한번 살펴보자. <input>태그를 하나의 컴포넌트로 생각해보자(input은 브라우저DOM에서 기본으로 제공하는 것이지만, 어쨋든 그 특징과 사용을 살펴보면 하나의 컴포넌트라고 생각할 수 있음). 우리는 onChange 속성에 함수를 값으로 주어서 <input>컴포넌트에 이벤트리스너를 달았다(리건 리액트 내부적으로 자동 실행되서 우리는 함수만 넘겨주면 되었음). 이 패턴을 우리의 컴포넌트에서도 사용할 수 있다. 

 

자식 컴포넌트에서 onChange와 같은 이벤트 props를 만들면, 부모는 그 값으로 함수를 지정할 수 있고 그러면 부모는 자식에게 해당 함수를 전달하게 되고 자식 컴포넌트에서는 그 함수를 호출하여 사용할 수 있게 된다. 그러면 자식 컴포넌트는 그 함수에 매개변수로 데이터를 전달할 수 있다.

(자식 컴포넌트에서 이벤트props 생성 -> 부모에서 자식 컴포넌트 사용 시, 이벤트 props에 함수 지정 -> 해당 함수가 자식 컴포넌트로 전달 -> 자식 컴포넌트에서 전달받은 함수 호출 가능 -> 해당 함수에 매개변수를 이용하여 데이터를 부모로 전달)

 

1. 먼저, ExpenseForm 컴포넌트에서 수집한 데이터 expenseData를 NewExpense 컴포넌트로 전달해보자(현재 컴포넌트 구조를 살펴보면 App-NewExpense-ExpenseForm으로 이어져 있어서, App까지 데이터를 전달하려면 반드시 NewExpense를 거쳐가야 함).

import React from "react";
import "./NewExpense.css";
import ExpenseForm from "./ExpenseForm";

const NewExpense = () => {
  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(),
    };
    console.log(expenseData);
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
    </div>
  );
};

export default NewExpense;

위 코드를 살펴보면 ExpenseForm 컴포넌트를 사용할 때 onSaveExpenseData 속성에 함수를 지정해주었다. 해당 함수(saveExpenseDataHandler를 살펴보면 enteredExpenseData라는 매개변수를 전달받아 그 값에 id 속성을 추가해서 expenseData 변수에 복사해준다. 그리고 expenseData를 콘솔창에 보여준다.

여기까지하면 상위 컴포넌트 NexExpense는 하위 컴포넌트 ExpenseForm으로 특정 함수(saveExpenseDataHandler)를 onSaveExpenseData라는 이름으로 전달하게 된다.

 

그럼 이제 하위 컴포넌트에서 이를 어떻게 사용하는지 살펴보자.

import React, { useState } from "react";
import "./ExpenseForm.css";

const ExpenseForm = (props) => {
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");
  const [enteredDate, setEnteredDate] = useState("");

  const titleChangeHandler = (event) => {
    setEnteredTitle(event.target.value);
  };
  const amountChangeHandler = (event) => {
    setEnteredAmount(event.target.value);
  };
  const dateChangeHandler = (event) => {
    setEnteredDate(event.target.value);
  };

  const submitHandler = (event) => {
    event.preventDefault();

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };

    props.onSaveExpenseData(expenseData);//
    setEnteredTitle("");
    setEnteredAmount("");
    setEnteredDate("");
  };
  return (
    <form onSubmit={submitHandler}>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input
            type="text"
            value={enteredTitle}
            onChange={titleChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Amount</label>
          <input
            type="number"
            min="0.01"
            step="0.01"
            value={enteredAmount}
            onChange={amountChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max="2022-12-31"
            value={enteredDate}
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

 이제 ExpenseForm 컴포넌트에서 props를 사용해야하므로 이를 추가해주었다. props를 이용해 onSaveExpenseData속성을 가져와야 한다. submitHandler함수를 살펴보면 기존의 데이터 콘솔 출력 코드 console.log(expenseData) 대신 다른 코드가 작성되어있다.

props.onSaveExpenseData(expenseData);

props로 onSaveExpenseData에 지정된 함수를 불러오고, 해당 함수가 매개변수를 사용하므로 그 매개변수에 ExpenseForm의 expenseData를 넣어주면 상위 컴포넌트에 작성된 함수에서 그 값을 복사해서 저장한다. 이렇게 하위 컴포넌트에서 상위 컴포넌트로 데이터를 전달하는 것이다.

 

여기까지 실행해보면,

콘솔창

ExpenseForm 컴포넌트에서 얻은 데이터가 상위 컴포넌트인 NewExpense컴포넌트에 전달되어 출력되는 것을 볼 수 있다(id도 추가된 거 확인!).

 

2. 이번엔 NewExpense 컴포넌트의 데이터를 App 컴포넌트로 전달해보자. 아까와 같은 방식이다.

import Expenses from "./components/Expenses/Expenses";
import NewExpense from "./components/NewExpense/NewExpense";

const App = () => {
  const expenses = [
    {
      id: "e1",
      title: "Toilet Paper",
      amount: 94.12,
      date: new Date(2020, 7, 14),
    },
    { id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12) },
    {
      id: "e3",
      title: "Car Insurance",
      amount: 294.67,
      date: new Date(2021, 2, 28),
    },
    {
      id: "e4",
      title: "New Desk (Wooden)",
      amount: 450,
      date: new Date(2021, 5, 12),
    },
  ];

  const addExpenseHandler = (expense) => {
    console.log("In App.js");
    console.log(expense);
  };
  return (
    <div className="App">
      <NewExpense onAddExpense={addExpenseHandler} />
      <Expenses items={expenses} />
    </div>
  );
};

export default App;

App 컴포넌트에서 NewExpense 컴포넌트로 특정함수(addExpenseHandler)를 onAddExpense라는 이름으로 전달한다. 그리고 addExpenseHandler가 매개변수를 가진다는 것 주의!

 

import React from "react";
import "./NewExpense.css";
import ExpenseForm from "./ExpenseForm";

const NewExpense = (props) => {
  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(),
    };
    props.onAddExpense(expenseData);
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
    </div>
  );
};

export default NewExpense;

NewExpense 컴포넌트에서는 saveExpenseDataHandler함수에서 그보다 하위 컴포넌트인 ExpenseForm으로부터 받아온 expenseData를 props를 통해서 상위 컴포넌트로 전달한다. props.onAddExpense는 매개변수를 가지니까 매개변수에 exepenseData 전달!

 

그리고 이렇게 실행해주면,

콘솔창

이번엔 데이터가 App 컴포넌트에서 출력되는 것을 볼 수 있다.

 


13. State 상위 컴포넌트로 옮기기(Lifting State Up)

12파트에서 한 것이 하위 컴포넌트에서 생성된 데이터를 상위 컴포넌트로 옮기는 작업이었다. 옮기기 작업을 왜 하는 것이지 다시 살펴보자.

일단 현재 프로젝트의 가장 상위 컴포넌트는 App이다. 이 앱은 Expenses와 NewExpense, 두 개의 컴포넌트를 가진다. NewExpense컴포넌트는 ExpenseForm컴포넌트를 이용하여 새로운 데이터를 생성한다. Expenses컴포넌트는 expenses데이터 배열을 이용하여 그 내용 화면에 보여주는 역할을 한다. 즉, NewExpense는 데이터를 생성하고 Expenses는 데이터를 사용한다.

Expenses컴포넌트는 NewExpense컴포넌트에서 새로 생성된 데이터도 화면에 보여주어야 하는데 두 컴포넌트 사이에는 직접적인 연결이 없다. 따라서 둘 사이의 직접적인(direct) 데이터 전달은 불가능하다. 그렇다면 둘을 이어주고 있는 App컴포넌트를 이용해야 한다.

따라서 이렇게 데이터를 전달하기 위해 우리는 NewExpense 컴포넌트의 데이터를 그 상위의 App 컴포넌트로 옮긴 것이다. 이렇게 전달받은 데이터를 props로 Expenses 컴포넌트로 내려 전달하면 Expenses 컴포넌트는 이를 활용해 해당 데이터를 화면에 렌더링하여 보일 수 있을 것이다(새로운 데이터를 expense 데이터 배열에 추가하여 렌더링하는 작업은 아직 안함).

 

이상, state을 lifting up한 이유!

 


14. 이벤트 및 State 작업하기

추가로 ExpensesFilter 컴포넌트에서 Expenses 컴포넌트로 데이터를 전달하여 해당 데이터를 state으로 저장해보자.

 

import React, { useState } from "react";
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";
import Card from "../UI/Card";
import ExpensesFilter from "./ExpensesFilter";

const Expenses = (props) => {
  const [filteredYear, setFilteredYear] = useState("2020");

  const filterChangeHandler = (selectedYear) => {
    setFilteredYear(selectedYear);
  };

  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter
          selected={filteredYear}
          onChangeFilter={filterChangeHandler}
        />
        <ExpenseItem
          title={props.items[0].title}
          amount={props.items[0].amount}
          date={props.items[0].date}
        />
        <ExpenseItem
          title={props.items[1].title}
          amount={props.items[1].amount}
          date={props.items[1].date}
        />
        <ExpenseItem
          title={props.items[2].title}
          amount={props.items[2].amount}
          date={props.items[2].date}
        />
        <ExpenseItem
          title={props.items[3].title}
          amount={props.items[3].amount}
          date={props.items[3].date}
        />
      </Card>
    </div>
  );
};

export default Expenses;

 

import React from "react";

import "./ExpensesFilter.css";

const ExpensesFilter = (props) => {
  const dropdownChangeHandler = (event) => {
    props.onChangeFilter(event.target.value);
  };
  return (
    <div className="expenses-filter">
      <div className="expenses-filter__control">
        <label>Filter by year</label>
        <select value={props.selected} onChange={dropdownChangeHandler}>
          <option value="2022">2022</option>
          <option value="2021">2021</option>
          <option value="2020">2020</option>
          <option value="2019">2019</option>
        </select>
      </div>
    </div>
  );
};

export default ExpensesFilter;

추가로 ExpensesFilter 컴포넌트 props에 selected속성을 추가하여 해당 selected속성 값으로 저장한 state을 지정해서 <select>태그의 valuer 값으로 했다. 이건 양방향 방인딩!

 


15. 제어된 컴포넌트와 제어되지 않은 컴포넌트 및 stateless 컴포넌트와 stateful 컴포넌트

1. 제어된 컴포넌트 vs 제어되지 않은 컴포넌트

  • "Expenses 컴포넌트가 ExpensesFilter 컴포넌트를 제어한다": ExpensesFilter 컴포넌트는 단지 filter UI를 화면에 보여주는 컴포넌트이다. 실제로 이를 기능하게 하는 것은 Expense 컴포넌트이다. ExpensesFilter의 onChange 이벤트 리스너는 props로 전달된 onChangeFilter에 의해 기능하고, <select>태그 value 역시 props로 전달된 selected 값으로 지정되기 때문!

2. stateless(dumb, presentational) 컴포넌트 vs stateful(smart) 컴포넌트

  • stateless 컴포넌트: 말 그대로, 아무 state도 가지지 않는 컴포넌트. 단지 데이터 출력을 위해 존재한다.
  • stateful 컴포넌트: 여기 말 그대로, 어떤 state을 가지는 컴포넌트. state을 관리한다. 이 state은 일반적으로 props에 의해 다른 컴포넌트로 분산된다.

   아마 프로젝트의 대부분은 stateless 컴포넌트이고, 일부만이 stateful 컴포넌트일 것이다.

 


이상, 여기까지 컴포넌트의 state를 알아보았습니다!!

.

.

.

휴~힘들다