내장된 html 요소는 모든 DOM 이벤트에 접근할 수 있다.
JSX 요소로 이동해서 이벤트 리스너를 추가하고 특별한 props를 추가한다.
모든 이벤트 핸들러 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의 글자는 바뀌지 않는다.
버튼을 눌렀을 때 화면에 보이는 것을 업데이트하고 싶다면, 리액트에게 어떤 것이 변경되었고 특정 컴포넌트가 재평가되어야 한다고 말하는 것이 필요하기 때문에 state를 사용한다.
react 라이브러리에서 useState를 임포트 (named import)
import React, { useState } from 'react';
컴포넌트 함수 안에서 useState 호출
useState()는 특별한 종류의 변수를 생성하고 괄호 안에 값을 넣어 초기값을 할당할 수 있다.
useState(props.title)
이 변수는 변경사항이 생기면 컴포넌트 함수가 다시 호출되도록 한다.
useState는 항상 두개의 요소가 있는 배열을 반환한다.
관리되고 있는 변수 (현재 상태값)
변수에 새로운 값을 할당하기 위해 호출할 수 있는 함수 (변수를 업데이트하는 함수)
const [title, setTitle] = useState(props.title)
setTitle 함수를 호출해서 새 값을 할당한다.
const clickHandler = () => {
setTitle('Updated!')
console.log(title)
}
setTitle을 사용하는 이유는 단지 어떤 변수에 새로운 값을 할당하는 것이 아니라 state를 업데이트해서 컴포넌트 함수를 다시 실행할 수 있기 때문이다.
이 때 console.log() 함수는 여전히 업데이트되기 전의 title을 보여준다.
정리
const [title, setTitle] = useState(props.title)
에서 const를 사용하는 이유는?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)
}
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>
}
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,
})
}
현재 코드는 이전 state에 의존하여 상태를 업데이트한다.
만약 하나씩 증가하는 카운터를 관리하고 있다면 상태를 업데이트할 때마다 이전 코드를 그대로 복사하여 붙여넣는 방식은 쓸 수 없다.
상태를 업데이트하는 함수 안에 함수를 작성해야 한다.
이전 state의 스냅샷을 받아와 새로운 state의 스냅샷을 반환한다.
setUserInput((prevState) => {
return { ...prevState, enteredTitle: event.target.value }
})
방법 3을 사용하는 이유
submit 버튼을 눌렀을 때 폼이 제출되고, 상태 조각들을 하나로 모아서 객체로 결합해보자.
onSubmit
브라우저에서는 폼이 제출될 때마다 웹페이지를 호스팅하고 있는 서버에 요청을 보내 페이지가 다시 로드된다.
폼이 제출됐을 때 객체를 생성
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 > NewExpense > App으로 데이터를 보내보자.
속성은 부모에서 자식으로만 전달될 수 있고 중간 컴포넌트를 생략할 수 없다.
ExpenseForm > NewExpense
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>
}
이제 이 함수를 수동으로 불러와 우리의 사용자 지정 컴포넌트에서 사용해야한다.
props.onSaveExpenseData()
로 실행할 수 있다.props.onSaveExpenseData(ExpenseData)
NewExpense > App
const addExpenseHandler = (expense) => {
console.log('In App.js')
console.log(expense)
}
<NewExpense onAddExpense={addExpenseHandler} />
props.onAddExpense(expenseData)
상태 끌어올리기란 자식 컴포넌트에서 부모 컴포넌트로 데이터를 이동해서 거기서 사용하거나 또는 다른 자식 컴포넌트로 데이터를 전달하는 것이다.
다른 형제 컴포넌트에서 바로 데이터를 넘겨받고 싶지만 컴포넌트는 부모와 자식 사이만 소통을 할 수 있다.
이런 경우에는 보통 가장 가까운 부모 컴포넌트를 활용한다.
현재 실습 파일에서 NewExpense에서 Expenses로 데이터를 보내주고 싶다면 App.js를 활용하여야 한다.
NewExpense는 props를 사용하여 onAddExpense함수를 호출할 수 있고, 호출하고 있는 함수에 데이터를 전달하여 상태를 끌어올린다.
Expense는 props를 이용하여 데이터를 전달받는다.
만약 두 자식 컴포넌트와 상호작용을 하는 App 컴포넌트를 갖고 있다면 이 방법은 작동하지 않는다.
항상 App 컴포넌트까지 상태를 끌어올려야 하는 것이 아니라 데이터를 생성하는 컴포넌트와 데이터가 필요한 컴포넌트에 접근할 수 있으면 된다.
필터 컴포넌트를 추가하여 연도에 따른 필터기능 추가
내가 작성한 코드
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>
);
}
정답 코드
수정사항
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