
이번에는 사용자들의 반응, 입력 등과 상호작용하는 페이지를 만들어보고자 한다.
아이템 목록 card마다 버튼을 만들어서 버튼을 클릭할 경우 자동으로 이름이 바뀔 수 있도록 해보자.
예시 페이지
리액트에서는 아래와 같이 title 변수를 생성해 title을 바꿨는데 화면에 반영이 되지 않는다. 그 이유는 리액트는 위에서부터 코드를 읽으며 렌더링 하는데 title이 바꼈다고 해서 그 코드로 돌아가 다시 랜더링을 하지 않기 때문이다.
const title = 'Update Title'
이를 위해서 사용하는 것이 useState 다.
useState란 리액트 라이브러리에서 제공하는 함수로 Hook이라고도 한다.
// useState 불러옴 import { useState } from "react"; // useState 구조 const [ 현재 상태값, 업데이트 함수 ] = useState(초기값)
이렇게 useState를 사용하면 state가 변경되었기 때문에 리액트가 컴포넌트 함수를 다시 호출하여 재렌더링(Re-render)을 한다. 이때 가져오는 state는 가장 최신의 state이다.
참고로 useState는 컴포넌트별 인스턴스를 기반으로 독립적인 state를 갖는다.
import { useState } from "react";
export default function ExpenseItem(props) {
const [title, setTitle] = useState(props.title);
const clickHandler = () => {
setTitle("updated!!");
};
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>
title을 바꾸는 코드는 위와 같다. 여기서 onClick은 <button> 컴포넌트를 눌렀을 때 설정한 함수를 실행해주는 props이다.
함수는 inline에 작성해도 되지만 코드가 깔끔하지 않기 때문에 return 상위에 변수로 선언해서 사용해준다.
이제 item을 추가할 수 있는 form을 만들어보자.

사용자가 input 창에 입력한 값을 관리하기 위해서는 onChange 를 사용해야 한다. onChange는 사용자가 input 창에 무언가를 입력할 때마다 함수가 실행 된다.
onChange도 앞서 사용했던 onClick과 같이 inline에 함수를 정의하지 않고 새로운 상수를 만들어 사용한다.
그전에 먼저 components 하위 폴더에 사용자가 입력한 값을 만드는 폼과 관련된 컴포넌트를 모아두는 NewExpense 폴더를 만들었다.
// NewExpense/ExpenseForm.js
const [enteredTitle, setEnteredTitle] = useState("");
const titleChangeHandler = (event) => {
// event.target.value = 사용자가 입력한 값
setEnteredTitle(event.target.value);
};
사용자가 입력한 값을 컴포넌트 수명과는 별개로 어떤 함수에 저장하기 위해 useState를 사용하였다.
title 뿐만 아니라 amount, date도 input에 입력 받은 값으로 변경되길 원하기 때문에 2개 더 추가하였다.
// NewExpense/ExpenseForm.js
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);
};
state와 각각의 eventHandler를 만들었는데, 똑같은 기능을 하는 코드가 반복되니 이를 더 간결하게 만들어보자.
const [userInput, setUserInput] = useState({
enteredTitle: "",
enteredAmount: "",
enteredDate: "",
});
const titleChangeHandler = (event) => {
setUserInput({
...userInput,
enteredTitle: event.target.value,
});
};
const amountChangeHandler = (event) => {
setUserInput({
...userInput,
enteredAmount: event.target.value,
});
};
const dateChangeHandler = (event) => {
setUserInput({
...userInput,
enteredDate: event.target.value,
});
처음 작성한 코드는 각각 useState를 사용했는데 두번재 작성한 코드는 하나의 useState안에 객체를 만들어 작성했다. 이후 setUserInput에도 마찬가지로 객체를 사용했는데, spread 연산자를 통해 기존에 갖고있던 값들을 가져오고 새로 입력 받은 값(event.target.value)을 넣어주었다.
그런데 위 코드도 약간의 문제가 있다. 만약 동시에 여러 상태를 업데이트를 하게된다면, 오래되거나 잘못된 상태 스냅샷에 의존하게 된다.
이를 해결하려면 아래와 같은 코드를 사용하면 된다.
const titleChangeHandler = (event) => {
setUserInput((prevState) => {
return { ...prevState, enteredTitle: event.target.value };
});
};
React는 이 함수에서 제공하는 상태 스냅샷이 항상 최신 상태 스냅샷이 되도록 보장하며 모든 예약된 상태 업데이트를 염두에 두고 있다. 그래서 항상 최신 상태 스냅샷에서 작업하도록 할 때는 위와 이렇게 코드를 작성하는 것이 좋다.
이처럼 state를 사용하는 방법 3가지를 알아봤는데 그 중 제일 처음 작성했던 코드를 사용하겠다.
이제 폼에 입력된 값들을 실제 화면에 나타내보려고 한다. 이때, click 버튼을 눌러 submitHandler 함수가 실행이 되면 브라우저는 폼이 제출될 때마다 서버에 요청을 보내고 페이지를 다시 로드한다. 그러면 사용자가 입력한 input 값이 없어지기 때문에 이를 방지하기 위해 preventdefault() 를 사용한다.
const submitHandler = (event) => {
event.preventdefault();
// 입력된 값들을 한번에 처리하기 위해 객체로 만들어줬다.
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
console.log(expenseData);
// 입력이 끝나면 input을 빈칸으로 만들어줌
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>
<div className="new-expense__actions">
<button type="submit">Add Expense</button>
</div>
</form>
);
위 코드에서 state를 사용했기 때문에
이를 양방향 바인딩이라고 한다.
이제 From을 통해 만들어진 data를 App 컴포넌트에 넘겨주자. 아래에서 코드를 보며 자식 -> 부모 로 data를 넘겨주는 걸 설명하고자 한다.
// NewExpense/NewExpense.js
export default function NewExpense(props) {
const saveExpenseDateHandler = (enteredExpenseData) => {
const expenseDate = {
// spread 연산자 사용
...enteredExpenseData,
id: Math.random().toString(),
};
// onAddExpense = App 컴포넌트에 있는 props
props.onAddExpense(expenseDate);
};
return (
<div className="new-expense">
// onSaveExpenseData props를 만들어 함수를 넣어줌
<ExpenseForm onSaveExpenseData={saveExpenseDateHandler} />
</div>
);
}
---------------------------------------------------------------------------
// NewExpense/ExpenseForm.js
export default function ExpenseForm(props) {
.
.
.
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
// NewExpense에 만든 props에 매개변수로 사용자가 입력한 data(expenseData)를 전달
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__actions">
<button type="submit">Add Expense</button>
</div>
</form>
부모 컴포넌트(NewExpense)에서 자식 컴포넌트(ExpenseForm)으로 함수를 넘겨주면 자식 컴포넌트는 그 함수를 호출할 수 있다. 이때 함수의 매개변수로 넘기고자 하는 data를 전달한다. 이렇게 하면 자식 컴포넌트의 data를 부모 컴포넌트, 최종적으로 App 컴포넌트에 넘겨줄 수 있다.
참고로 onSaveExpenseData 는 우리가 만든 컴포넌트(ExpenseForm)의 props 이기 때문에 원하는 이름으로 작성을 했다.
정리하여 설명하자면,