리액트 State및 이벤트

ZeroJun·2022년 6월 2일
0

React

목록 보기
3/13

리액트 jsx에서 element에 이벤트를 추가하는 것은 html과 거의 같다.
모든 이벤트는 on으로 시작하며 on뒤에 나오는 단어는 대문자로 시작한다.

 <button onClick={() => console.log("클릭완료")}>Change Tilte</button>

그러나 jsx에 많은 로직이 포함되는 것은 좋지 않기 때문에 아래와 같이 코드를 작성하는 것이 좋다.

function 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 Tilte</button>
    </Card>
  );
}

이벤트를 통한 element상태 변경

Dom 변경 실패 코드

function ExpenseItem(props) {
  let title = props.title; // props를 title에 담는다
  const clickHandler = () => {
    title = "update!"; // 클릭 시 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 Tilte</button>
    </Card>
  );
}

위와 같이 코드를 작성하면 클릭을 해도 h2태그의 title이 변하지 않는다.

그 이유는 무엇일까?

리액트는 jsx를 읽어나가면서 컴포넌트들을 만날때마다 컴포넌트를 호출하고, 더 이상 읽을 jsx가 없을 때 까지 그것을 이어간다. 리액트가 실행되고, App에서 ExpenseDate까지 랜더링이 완료된 후 컴포넌트 호출은 멈춘다.

그래서 위의 방식대로 변수를 선언하고, 클릭을 한다고 해서 컴포넌트가 호출 되지는 않는다. 단지 컴포넌트 내의 특정 함수가 실행될 뿐 컴포넌트 자체가 호출되지 않는 것이다. 따라서 위이 코드에서 h2태크의 content가 변하지 않은 것이다.

그래서 컴포넌트의 특정 요소를 바꾸기 위해선 컴포넌트를 호출하여 재평가하는 방법이 필요하다.
그것이 바로 state다.

State 사용법

useState함수 : 리액트 훅 중 가장 중요한 것이며 컴포넌트의 함수가 다시 호출되는 곳에서 변경된 값을 반영하기 위해 state로 값을 정의할 수 있게 해주는 함수다.

useState는 컴포넌트 외부 혹은 컴포넌트가 가지고 있는 중첩된 함수 내부에서 호출하면 안되고, 컴포넌트에서 다이렉트로 호출되어야 한다.

useState는 항상 크기가 2인 배열을 반환하며 첫번째는 변수 자체(현재 상태값), 두번째는 업데이트 되는 함수를 담고있다.
그래서 구조분해 할당을 통해 return되는 요소를 받을 수 있다.

const [관리대상변수, 업데이트함수] = useState(관리대상의초기값);

여기서 업데이트함수가 호출되면 컴포넌트가 재평가된다. 즉 컴포넌트가 다시 실행되면서 jsx코드를 다시 평가한다. 이 때, 업데이트 함수의 인자가 관리대상변수에 할당을 예약하게된다.

import React, { useState } from 'react'; 
// react라이브러리로부터 useState함수를 import한다.

function ExpenseItem(props) {
  const [title, setTitle] = useState(props.title);
  const clickHandler = () => {
    setTitle('Update!'); 
    // 이 호출로 인해 컴포넌트는 재랜더링 되고, title에 'Update!'가 할당이 예약되게 된다.
    title === 'Update!' 
    // false : 아직 할당된 것은 아니다.
    // 재 랜더링 되면서 할당된다.
  };
  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 Tilte</button>
    </Card>
  );
}

useState 자세히 알아보기

지금 진행하고 있는 프로젝트에선 ExpenseItem이 동일한 컴포넌트에 의해 4번 생성되었다. 동일한 컴포넌트기 때문에 useState를 통해 title을 변경하면 모든 ExpenseItem이 똑같이 변경되야 할 것 같지만 따로 변경된다. 그 이유는 동일한 컴포넌트가 여러번 생성되더라도 리액트에 의해 독립적으로 관리되기 때문이다.

즉 state는 컴포넌트의 인스턴스 별로 나뉘어져 있다.

useState는 컴포넌트가 처음 실행될 때 초기값을 설정한 이후로 재 랜더링 될때 state를 다시 초기화 하지 않는다. 랜더링 될 때마다 setTitle에 의해 업데이트된 가장 최신의 state가 반영 된다.

그리고, 컴포넌트 내에 여러개의 useState를 두어도 개별적으로 잘 관리된다.

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

여러개의 상태가 있을 때 아래와 같이 사용할 수도 있다.

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

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

  const dateChangeHandler = (event) => {
    setUserInput((prevState) => {
      return {...prevState, enteredDate: event.target.value}
    })
  };
// 이렇게 setState함수안에 직접 인자를 넣기 보다 
// 기존 상태를 인자로 받은 함수를 리턴하는 것은 가장 최신의 스냅샷을 보장하는 보다 안전한 방법이다.
// 이전 다른 요소의 state에 의존해야할 경우 이런 방법을 사용한다.

자주 쓰이는 이벤트

  • onChange : 모든 입력 타입에 사용할 수 있다.
// jsx
<input type="text" onChange={tilteCangeHandler} />

const tilteCangeHandler = (event) => {
  console.log(event.target.value); // 입력할 때마다 입력한 문자 출력
};
  • onSubmit : 폼이 제출될 때마다 일부 함수를 실행한다.
  const submitHandler = (event) => {
    event.preventDefault(); // 페이지 리로드 방지

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

  return  <form onSubmit={submitHandler}>

양방향 바인딩

양방향 바인딩은 폼을 제출하거나 전송할 때, input의 입력 값만 수신하는 것이 아니라 input에 새로운 값을 다시 전달할 수 있는 것이다.

아래 처럼 input 엘리먼트의 value에 setState변수를 추가하면 양방향 바인딩이 가능한 상태가 된다.

const [enteredTitle, setEnteredTitle] = useState("");

setEnteredTitle(''); 
// 이렇게 하면 enteredTitle를 value로 설정한 모든 엘리먼트의 content가 ""로 바뀐다.

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

자식-부모 컴포넌트 통신(상향식)

  • remind : 속성은 오로지 부모에서 자식으로만 전달될 수 있다. 즉, 중간 컴포넌트를 생략할 수는 없다.
  1. input요소가 존재하는 NewExpenseForm에서 NewExpenseForm을 감싸고 있는 NewExpense로 데이터 보내기
// NewExpense.js
const NewExpense = () => {
  // 자식에서 호출할 함수
  const saveExpenseDataHandler = (enteredExpenseData) => { 
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(), // 이론적으로 같은 id가 생성될 수 있어서 완벽하게 고유한 id는 아니다.
    };
    console.log(expenseData);
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
      {/* ExpenseForm(자식)에게 props를 통해 함수를 전달한다. */} 
    </div>
  );
};
const ExpenseForm = (props) => { // props로 인자를 받는다.
    const submitHandler = (event) => {
    event.preventDefault(); // 페이지 리로드 방지

    const expenseData = {
      title: enteredTitle,
      amount: enteredAmount,
      date: new Date(enteredDate),
    };
    props.onSaveExpenseData(expenseData);
    // NewExpense(부모)로부터 받은 함수에
    //ExpenseForm의 expenseData속 객체를 인자로 넣어서 호출할 수 있다.
    // 즉, 부모컴포넌트의 함수를 자식 컴포넌트에서 호출하는 방법이다.
    // 또한 자식 컴포넌트의 데이터를 부모컨테이너로 전달하는 방법이다.
    setEnteredTitle("");
    setEnteredAmount("");
    setEnteredDate("");
  };
}
  1. NewExpense에서 App으로 데이터 보내기 (1번의 과정과 동일하다.)
// App.js
  const addExpenseHandler = (expense) => { // NewExpense에서 호출할 함수
    console.log("In App.js");
    console.log(expense);
  };
  return (
    <div>
      <NewExpense onAddExpense={addExpenseHandler} />
      {/* ExpenseForm(자식)에게 props를 통해 함수를 전달한다. */} 
      <Expenses items={expenses} />
    </div>
  );
};
// NewExpense.js
const NewExpense = (props) => { // props로 인자를 받는다.
  const saveExpenseDataHandler = (enteredExpenseData) => {
    const expenseData = {
      ...enteredExpenseData,
      id: Math.random().toString(), 
    };
    props.onAddExpense(expenseData); 
    // App의 함수에 expenseData를 넣어서 호출
  };
  return (
    <div className="new-expense">
      <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
    </div>
  );
};

이렇게 NewExpenseForm에서 생성한 객체를 NewExpense에 전달하고, 그 객체에 id를 더해서 App에 전달하게 된다. (NewExpenseForm -> NewExpense -> App)

정리하면 props을 이용하기 위해 자식 컴포넌트의 프로퍼티에 함수를 부여하고, 자식 컴포넌트에서 만들어진 데이터를 인자로 넣어 해당 함수를 호출하는 패턴이다.

State 끌어올리기

리액트는 항상 부모-자식 간 데이터 이동이 가능하므로 형제 컴포넌트에게 데이터를 넘겨주기 위해선 언제나 부모 컴포넌트를 경유해야 한다. 데이터를 이동시킬 땐 항상 최상단 컴포넌트인 App컴포넌트까지 끌어올려야 하는 것이 아니고, 목표 컴포넌트로 전달될 수 있을 정도로만 끌어 올리면 된다.

제어된 컴포넌트와 제어되지 않은 컴포넌트 및 Stateless컴포넌트와 Stateful컴포넌트

상태를 가지고 있지 않고 그저 출력만 하는 컴포넌트를 Stateless컴포넌트라고 한다.

제어된 컴포넌트는 아래와 같다.

ExpensesFilter컴포넌트를 제어(관리)하는 Expenses컴포넌트

function Expenses(props) {
  const [filteredYear, setFilteredYear] = useState("2022");
  const filterChangeHandler = (selectedYear) => {
    setFilteredYear(selectedYear);
  };

  // map활용
  const ExpenseItemList = props.items.map((el, key) => (
    <ExpenseItem key={key} title={el.title} amount={el.amount} date={el.date} />
  ));

  return (
    <div>
      <Card className="expenses">
        <ExpensesFilter {/* ExpensesFilter를 제어 */}
          selected={filteredYear}
          onChangeFilter={filterChangeHandler}
        />
        {ExpenseItemList}
      </Card>
    </div>
  );
}

제어된 컴포넌트인 ExpensesFilter

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;

Expenses컴포넌트에서는 filteredYear를 전달하고, 다시 filteredYear의 상태 값은 props를 통해 ExpensesFilter로 돌아온다.

리액트에선 대부분 Stateless컴포넌트로 이루어져 있으며 일부 컴포넌트만 state를 가지고 있고, state는 props를 통해 분산되게 된다.

0개의 댓글