리액트 State 및 이벤트 다루기

김지영·2022년 6월 23일
0

React-study

목록 보기
2/5

이벤트 리스닝 & 이벤트 핸들러

  • 내장된 html 요소는 모든 DOM 이벤트에 접근할 수 있다.

  • JSX 요소로 이동해서 이벤트 리스너를 추가하고 특별한 props를 추가한다.

    • 리액트는 모든 기본 이벤트를 on으로 시작하는 props로 노출한다. (onClick)
    • 이벤트를 수신했을 때 실행될 함수를 작성한다.
  • 모든 이벤트 핸들러 props는 값으로 함수가 필요하고, on props에 대한 값으로 전달된 함수는 이벤트가 발생했을 때 실행되어야 한다.

  • <button onClick={() => { console.log('Clicked!') }}>Change Title</button>
  • 이렇게 JSX 코드 안에 함수를 넣는 것 보다는 함수를 미리 정의하는 것을 권장

    • 이 때 onClick={clickHandler}의 함수명 뒤에 괄호를 추가하지 않는다.
    • 코드 라인이 평가됐을 때 함수가 실행되는 것이 아니라 이벤트가 일어났을 때 함수가 실행되어야 하기 때문

컴포넌트 기능 실행

  • props.title을 변수에 정의하고 변수 값을 받도록 한다.

    • 변수이기 때문에 clickHandler가 실행될 때마다 변수에 값을 넣어서 바꿀 수 있다.

    • 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 >
        );
      }
    • 버튼을 누르면 콘솔창에 변경된 title이 표시되는 것을 볼 수 있다.

    • 그러나 DOM의 글자는 바뀌지 않는다.

      • 컴포넌트는 JSX코드를 반환하는 함수로 이루어져 있기 때문에 컴포넌트 함수를 호출해야 실행이 된다.
      • 호출된 컴포넌트 함수들은 더이상 호출한 컴포넌트 코드가 없을 때까지 평가된 JSX 코드를 반환한다.
      • 리액트는 응용프로그램이 처음 렌더링되었을 때 이러한 모든 과정을 실행하고 다시 반복하지 않는다.

State

  • 버튼을 눌렀을 때 화면에 보이는 것을 업데이트하고 싶다면, 리액트에게 어떤 것이 변경되었고 특정 컴포넌트가 재평가되어야 한다고 말하는 것이 필요하기 때문에 state를 사용한다.

  • react 라이브러리에서 useState를 임포트 (named import)

    • import React, { useState } from 'react';
  • 컴포넌트 함수 안에서 useState 호출

    • useState는 리액트 훅이고, 리액트 훅은 모두 이름이 'use'로 시작한다.
    • 함수 밖에서 호출하거나, 중첩된 함수 안에서 호출할 수 없고 컴포넌트 함수 안에서 직접적으로 호출되어야 한다.
  • useState()는 특별한 종류의 변수를 생성하고 괄호 안에 값을 넣어 초기값을 할당할 수 있다.

    • useState(props.title)
  • 이 변수는 변경사항이 생기면 컴포넌트 함수가 다시 호출되도록 한다.

  • useState는 항상 두개의 요소가 있는 배열을 반환한다.

    • 관리되고 있는 변수 (현재 상태값)

    • 변수에 새로운 값을 할당하기 위해 호출할 수 있는 함수 (변수를 업데이트하는 함수)

    • const [title, setTitle] = useState(props.title)
  • setTitle 함수를 호출해서 새 값을 할당한다.

    • const clickHandler = () => {
          setTitle('Updated!')
          console.log(title)
      }
    • setTitle을 사용하는 이유는 단지 어떤 변수에 새로운 값을 할당하는 것이 아니라 state를 업데이트해서 컴포넌트 함수를 다시 실행할 수 있기 때문이다.

  • 이 때 console.log() 함수는 여전히 업데이트되기 전의 title을 보여준다.

    • state를 업데이트하는 함수를 호출했을 때 바로 값을 바꾸지 않고 state의 업데이트를 예약한다.
  • 정리

    • 변화하는 데이터를 갖고 있는데 이러한 데이터가 사용자 인터페이스에 반영되어야 한다면 state가 필요하다.
    • 일반적인 변수와 달리 state를 사용하면 값을 설정하고 변경할 수 있다.
    • state가 바뀌면 리액트는 state가 등록된 컴포넌트를 재평가한다.

useState

  • Expenses.js는 네 개의 ExpenseItem을 갖고 있고 이 아이템들은 자신만의 state를 갖고 있어서 첫 번째 ExpenseItem에서 title을 변경해도 다른 아이템들은 영향을 받지 않는다.
  • 컴포넌트별 인스턴스를 기반으로 하여 각각 독립적인 state를 갖는다.
  • 리액트는 useState를 이용해서 상태가 바뀔 때마다 이 컴포넌트형 함수와 이 컴포넌트가 사용되는 곳에 있는 특정 인스턴스에서만 그것을 재평가한다.
  • const [title, setTitle] = useState(props.title)에서 const를 사용하는 이유는?
    • 변수에 값을 할당할 때 setTitle이라는 함수를 호출한다.
    • 등호 연산자를 이용해서 변수에 새로운 값을 할당하지 않으므로 const를 사용해도 된다.

input form

  • NewExpense.js

    • import React from 'react';
      
      import ExpenseForm from './ExpenseForm';
      import './NewExpense.css'
      
      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

사용자 입력 리스닝

  • onChange 이벤트

    • 모든 입력 타입에 같은 이벤트를 사용할 수 있다는 장점이 있다.

    • onChange 이벤트가 일어나면 발생하는 함수 titleChangeHandler를 생성한다.

    • 사용자가 입력한 값을 출력하고 싶다면?

      • 바닐라 자바스크립트와 동일하게 event라는 인수로 이벤트 객체를 얻을 수 있다.

      • event.input.value에서 이벤트가 벌어졌을 시점의 현재 입력값을 갖는다.

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

방법 1: 여러 state 다루기

  • useState 사용해서 입력된 값을 저장하여 submit 했을 때 모든 입력값들을 모아 객체에 결합하여 사용하도록 해보자.

    • useState를 사용하는 이유는 단지 컴포넌트를 업데이트하기 위해서가 아니라 컴포넌트 함수의 수명 주기와 관계없는 어떤 변수에 저장하기 위해서이다.

    • 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)
        }
      
        return <form>
          <div className="new-expense__controls">
            <div className="new-expense__control">
              <label>Title</label>
              <input type="text" onChange={titleChangeHandler} />
            </div>
            <div className="new-expense__control">
              <label>Amount</label>
              <input type="number" min="0.01" step="0.01" onChange={amountChangeHandler} />
            </div>
            <div className="new-expense__control">
              <label>Date</label>
              <input type="date" min="2019-01-01" max="2022-12-31" onChange={dateChangeHandler} />
            </div>
          </div>
          <div className="new-expense__actions">
            <button type="submit">Add Expense</button>
          </div>
        </form>
      }

방법 2: 한가지 state 다루기

  • state를 세개 생성하지 않고 useState의 값으로 객체를 전달하여 하나의 state를 사용하는 방법도 있다.

  • const [userInput, setUserInput] = useState({
        enteredTitle: '',
        enteredAmount: '',
        enteredDate: ''
    })
  • setUserInput 함수로 userInput의 값을 변경할 때 다른 두 데이터를 잃어버리지 않도록 주의해야한다.

    • 새로운 사용자 입력을 객체에 설정한다면 기본적으로 다른 키들은 버리게 된다.

    • state를 업데이트할 때 이전 state와 병합하지 않기 때문

    • 업데이트하지 않는 다른 값들을 수동으로 복사해야 한다.

    • const titleChangeHandler = (event) => {
          // setEnteredTitle(event.target.value)
          setUserInput({
              ...userInput,
              enteredTitle: event.target.value,
          })
      }

방법 3: 이전 state에 의존하지 않는 state

  • 현재 코드는 이전 state에 의존하여 상태를 업데이트한다.

  • 만약 하나씩 증가하는 카운터를 관리하고 있다면 상태를 업데이트할 때마다 이전 코드를 그대로 복사하여 붙여넣는 방식은 쓸 수 없다.

  • 상태를 업데이트하는 함수 안에 함수를 작성해야 한다.

    • 이전 state의 스냅샷을 받아와 새로운 state의 스냅샷을 반환한다.

    • setUserInput((prevState) => {
          return { ...prevState, enteredTitle: event.target.value }
      })
  • 방법 3을 사용하는 이유

    • 방법 2와 방법 3 모두 괜찮지만, 앞에서 말한 것처럼 리액트는 상태 업데이트 스케줄을 갖고 있어서 바로 실행되지 않는다.
    • 동시에 수많은 상태 업데이트를 계획한다면 오래되었거나 잘못된 상태 스냅샷에 의존할 수도 있다.
    • 방법 3을 사용한다면 리액트는 이 안에 있는 함수에서 이 상태 스냅샷이 가장 최신 상태의 스냅샷이라는 것과 항상 계획된 상태 업데이트를 염두에 두고 있다는 것을 보장한다.

input submit

  • submit 버튼을 눌렀을 때 폼이 제출되고, 상태 조각들을 하나로 모아서 객체로 결합해보자.

  • onSubmit

    • 폼이 제출될 때마다 어떠한 함수를 실행한다.
  • 브라우저에서는 폼이 제출될 때마다 웹페이지를 호스팅하고 있는 서버에 요청을 보내 페이지가 다시 로드된다.

    • 우리는 페이지가 다시 로드되지 않고 데이터를 수집하고 결합해서 저장하고 싶다.
    • 이러한 기본 동작을 event의 preventDefault메소드를 통해 막을 수 있다.
  • 폼이 제출됐을 때 객체를 생성

    • const submitHandler = (event) => {
          event.preventDefault()
          const ExpenseData = {
              title: enteredTitle,
              amount: enteredAmount,
              date: new Date(enteredDate)
          }
      }
    • date는 new date를 사용하여 내장된 날짜 생성자로 날짜 문자열을 분석해서 날짜 객체로 변환한 enteredDate를 전달한다.

양방향 바인딩

  • 폼을 제출했을 때 input form을 초기화하자.

  • state를 사용하면 양방향 바인딩을 구현할 수 있다.

  • 양방향 바인딩: 변경되는 입력값만 수신하는 것이 아니라 입력에 새로운 값을 다시 전달할 수도 있는 것

  • 입력요소에 value를 추가한다.

    • value

      • 모든 입력 요소들이 갖는 내부 값의 프로퍼티를 설정한다.
      • 요소들을 새로운 값으로 설정한다.
    • <input type="text" value={enteredTitle} onChange={titleChangeHandler} />
      • 양방향 바인딩
      • 상태를 업데이트하기 위해 입력에서 변경사항을 수신한다.
      • 입력에 상태를 다시 보내줘서 상태를 변경하면 입력도 변한다.
  • 폼이 제출되었을 때 setEnteredTitle을 호출하여 enteredTitle을 빈 문자열로 설정하면 입력부분도 지워진다.

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

  • 지금까지 입력된 데이터를 ExpenseForm 컴포넌트에 저장했는데 이러한 데이터를 App.js에 넘겨줘야 사용자가 입력한 새로운 비용을 기존의 비용 목록에 추가할 수 있다.
  • 지금까지는 부모 컴포넌트에서 자식 컴포넌트로 props를 사용하여 아래로 넘겨주었다.
  • 자식에서 부모로 데이터를 전달하려면?
    • 이벤트리스너를 갖는 input을 컴포넌트라고 생각하면 비슷한 방식이라고 볼 수 있다.
    • 자체 이벤트 속성을 생성하고 호출을 하고 싶으면 값으로 함수를 가질 수 있다.
    • 부모 컴포넌트로부터 자식 컴포넌트로 함수를 전달할 수 있게 한다.
    • 자식 컴포넌트에서 함수를 호출할 수 있다.
    • 함수를 호출했을 때 함수의 매개변수로 데이터를 전달할 수 있다.

실습: ExpenseForm > NewExpense > App으로 데이터를 보내보자.

  • 속성은 부모에서 자식으로만 전달될 수 있고 중간 컴포넌트를 생략할 수 없다.

  • ExpenseForm > NewExpense

    • ExpenseForm에 속성을 부여하고 이 속성에 대한 값은 함수여야 한다.

    • 이 함수는 컴포넌트 내부에서 어떤 일이 벌어졌을 때 작동되는 함수이다.

      • 여기서는 ExpenseData 폼이 제출될 때
    • const NewExpense = () => {
        const saveExpenseDataHandler = (enteredExpenseData) => {
          const expenseData = {
            ...enteredExpenseData,
            id: Math.random().toString()
          }
          console.log(expenseData);
        }
        return <div className="new-expense">
          <ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
        </div>
      }
    • 이제 이 함수를 수동으로 불러와 우리의 사용자 지정 컴포넌트에서 사용해야한다.

      • ExpenseForm에는 onSaveExpenseData라는 속성이 있기 때문에 props를 통해 접근할 수 있다.
      • 또한 이것은 함수이기 때문에 props.onSaveExpenseData()로 실행할 수 있다.
      • 또 ExpenseForm에서 생성한 expenseData를 인자로 전달할 수 있다.
      • 이 값을 NewExpense에서 매개변수로 받는 것이다.
    • props.onSaveExpenseData(ExpenseData)
  • NewExpense > App

    • const addExpenseHandler = (expense) => {
          console.log('In App.js')
          console.log(expense)
      }
      
      <NewExpense onAddExpense={addExpenseHandler} />
    • props.onAddExpense(expenseData)

State 끌어올리기

  • 상태 끌어올리기란 자식 컴포넌트에서 부모 컴포넌트로 데이터를 이동해서 거기서 사용하거나 또는 다른 자식 컴포넌트로 데이터를 전달하는 것이다.

  • 다른 형제 컴포넌트에서 바로 데이터를 넘겨받고 싶지만 컴포넌트는 부모와 자식 사이만 소통을 할 수 있다.

  • 이런 경우에는 보통 가장 가까운 부모 컴포넌트를 활용한다.

  • 현재 실습 파일에서 NewExpense에서 Expenses로 데이터를 보내주고 싶다면 App.js를 활용하여야 한다.

  • NewExpense는 props를 사용하여 onAddExpense함수를 호출할 수 있고, 호출하고 있는 함수에 데이터를 전달하여 상태를 끌어올린다.

  • Expense는 props를 이용하여 데이터를 전달받는다.

  • 만약 두 자식 컴포넌트와 상호작용을 하는 App 컴포넌트를 갖고 있다면 이 방법은 작동하지 않는다.

  • 항상 App 컴포넌트까지 상태를 끌어올려야 하는 것이 아니라 데이터를 생성하는 컴포넌트와 데이터가 필요한 컴포넌트에 접근할 수 있으면 된다.

연습하기

  • 필터 컴포넌트를 추가하여 연도에 따른 필터기능 추가

    • ExpenseFilter로부터 선택한 연도를 Expenses 컴포넌트에 보내서 상태를 저장한다.
  • 내가 작성한 코드

    • const ExpenseFilter = (props) => {
        const yearFilterHander = (event) => {
          props.onYearFilter(event)
        }
        return (
          <select name="years" onChange={yearFilterHander}>
            <option value="2019">2019</option>
            <option value="2020">2020</option>
            <option value="2021">2021</option>
          </select>
        )
      }
      
      export default ExpenseFilter
    • const Expenses = (props) => {
        const selectYearFilter = (year) => {
          console.log(year)
        }
        return (
          <div>
            <ExpenseFilter onYearFilter={selectYearFilter} />
            <Card className="expenses">
      		...
            </Card>
          </div>
        );
      }
  • 정답 코드

    • 수정사항

      • useState를 이용하여 state에 저장
      • 좀더 깔끔한 변수, 함수 이름
      • selected 속성을 이용하여 state의 초기값을 저장하고, ExpensesFilter에서 양방향 바인딩 추가
    • const Expenses = (props) => {
        const [filteredYear, setFilteredYear] = useState('2020')
        const filterChangeHandler = (selectedYear) => {
          setFilteredYear(selectedYear)
        }
        return (
          <div>
            <Card className="expenses">
              <ExpensesFilter selected={filteredYear} onChangeFilter={filterChangeHandler} />
      			...
            </Card>
          </div>
        );
      }
    • import React from 'react';
      
      import './ExpensesFilter.css'
      
      const ExpenseFilter = (props) => {
        const dropdownChangeHander = (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={dropdownChangeHander}>
                <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 ExpenseFilter

Stateless 컴포넌트와 Stateful 컴포넌트

  • 무상태 vs 상태 컴포넌트
  • 우리가 만드는 모든 리액트 앱에는 일부 상태를 관리하는 몇 개의 컴포넌트를 갖게 된다.
  • 상태를 관리하는 state가 없는 컴포넌트를 무상태 컴포넌트라고 한다.
    • 프리젠테이셔널 또는 dumb 컴포넌트라고도 불린다.
    • 아무 상태를 갖지 않고 데이터를 출력하기 위해 존재하기 때문
  • 대부분의 리액트 프로젝트에서 상태 유지 컴포넌트보다 무상태 컴포넌트를 더 많이 갖게 될 것이다.
    • 대부분의 컴포넌트들은 무언가를 출력하는데에 초점을 맞추고있기 때문

0개의 댓글