기본적으로 일반적인 변수들의 변경이 되어도 리액트는 이를 무시하고 코드가 실행되어도 신경쓰지 않으며 컴포넌가 다시 실행되지 않는다.
함수 컴포넌트는 처음에 렌더링 된 후에 다시 호출되지 않는다. 클릭했을 때나 변수가 변경되었을 때 해당 함수 컴포넌트를 다시 실행하라고 트리거하지 않기 때문이다. 이때 useState 훅을 사용하여 특정 상태가 변경되었을 때 해당 컴포넌트를 재호출할 수 있다.
function ExpenseItem(props) {
let title = props.title
const clickHandler = () => {
// 해당 함수가 실행되어 title 변수를 변경해도 ExpenseItem 컴포넌트를 다시 실행하라고 트리거 하지 않기 때문에
// ExpenseItem 컴포넌트를 다시 호출되지 않아 title 값이 변경되지 않은채 그대로 렌더링된다.
title = 'Updated !'
console.log('Clicked !!')
}
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는 컴포넌트 밖이 아닌 안에서 호출해야하며 중첩된 함수 안에서도 호출할 수 없고 컴포넌트 함수 안에서 직접적으로 호출되어야 한다.
import React, { useState } from 'react'
useState() // 컴포넌트 외부에서 호출할 수 없다.
function ExpenseItem(props) {
useState() // 컴포넌트 바로 안에서 직접적으로 호출해야 한다.
let title = props.title
const clickHandler = () => {
useState() // 중첩된 함수 안에서도 호출할 수 없다.
console.log('Clicked !!')
}
...
}
useState의 변수는 변경되면 해당 컴포넌트가 다시 호출되도록 한다.
만약 변화하는 데이터를 갖고 있는데 그 데이터가 바뀔 때마다 사용자 인터페이스에 반영되어야 한다면 state가 필요한 것이다.
위에서 console.log()는 여전히 업데이트되기 전의 값을 출력하고 있는데 state를 업데이트하는 clickHandler 함수를 호출 했을 때 사실상 바로 값을 바꾸지 않고 대신 이 state의 업데이트를 예약하기 때문이다. 그래서 다음 줄에서는 새로운 값이 아직 사용가능하지 않다.
아래와 같이 컴포넌트에서 콘솔 출력을 하는 코드가 있을 때 해당 컴포넌트가 상위에서 여러 번 사용되면 그 사용 횟수만큼 콘솔 출력이된다.
import React, { useState } from 'react'
import Card from '../UI/Card'
import ExpenseDate from './ExpenseDate'
import './ExpenseItem.css'
function ExpenseItem(props) {
const [title, setTitle] = useState(props.title)
console.log('ExpenseItem evaluated by React')
...
}
export default ExpenseItem
import React from 'react'
import Card from '../UI/Card'
import ExpenseItem from './ExpenseItem'
import './Expenses.css'
function Expenses(props) {
return (
<Card className="expenses">
// 1
<ExpenseItem
title={props.expenses[0].title}
amount={props.expenses[0].amount}
date={props.expenses[0].date}
/>
// 2
<ExpenseItem
title={props.expenses[1].title}
amount={props.expenses[1].amount}
date={props.expenses[1].date}
/>
// 3
<ExpenseItem
title={props.expenses[2].title}
amount={props.expenses[2].amount}
date={props.expenses[2].date}
/>
// 4
<ExpenseItem
title={props.expenses[3].title}
amount={props.expenses[3].amount}
date={props.expenses[3].date}
/>
</Card>
)
}
export default Expenses

하지만 특정 컴포넌트의 state가 변경이 되면 해당 컴포넌트만 재실행 되기 때문에 상위 컴포넌트에서 여러 번 사용되어도 콘솔은 한 번만 실행되어 출력된다.

하나의 컴포넌트에서 여러 개의 state를 다루기 위해서 useState 훅을 여러 번 사용할 수도 있다.
const [enteredTitle, setEnteredTitle] = useState('')
const [enteredAmount, setEnteredAmount] = useState('')
const [enteredDate, setEnteredDate] = useState('')
만약 여러 state가 서로 관련이 있다면 (위 코드에서는 세 state가 모두 폼과 관련이 있다고 가정한다.) 같은 개념이 반복 되는 것이기 때문에 값으로 객체를 전달하는 방식을 사용할 수도 있다.
const [userInput, setUserInput] = useState({
endteredTitle: '',
enteredAmount: '',
enteredDate: '',
})
이 방법의 주의할 점은 이 state를 변경하는 이벤트 핸들러에서 이 객체를 변경시킬 때 다음과 같이 변경시킬 프로퍼티 키만 입력하게 되면 기존 3개의 프로퍼티를 갖는 상태가 1개의 프로퍼티를 갖는 상태로 바뀌게 된다는 것이다.
const amountChangeHandler = (e) => {
setUserInput({
// state의 기본 상태가 나머지 다른 프로퍼티는 사라지고 enteredAmount 하나만 남게 된다.
enteredAmount: e.target.value,
})
}
따라서 다른 데이터(변경시킬 이외의 프로퍼티)들을 잃어버리지 않기 위해 객체를 복사한 뒤 내가 변경할 프로퍼티만 오버라이드 하면 덮어쓰게 돼서 입력한 프로퍼티의 값만 변경이 된다.
const titleChangeHandler = (e) => {
setUserInput({
// 복사
...userInput,
// 오버라이드
enteredAmount: e.target.value,
})
}
하지만 위와 같은 방법에는 기술적으로 문제는 없지만 특정 사례에서는 실패할 수도 있다.
상태를 업데이트하는 함수를 위한 대체 폼을 사용해야 한다. 상태를 업데이트하는 함수를 호출해서 값을 바로 넣어주는 것이 아닌 호출해서 그 안에 함수를 전달해야 한다. 전달되는 함수는 이전 state를 전달받고 이를 기반으로 새로운 state를 반환해야 한다.
const titleChangeHandler = (e) => {
setUserInput((prevState) => {
return { ...prevState, enteredAmount: e.target.value }
})
}
이렇게 하는 이유는 setState(num + 1)와 같이 콜백 함수 없이 setState를 사용하면 React는 비동기적으로 상태를 업데이트한다. 이는 React가 여러 setState 호출을 모아서 처리하기 위함이다.
상태 업데이트가 즉시 이루어지지 않고 비동기로 처리되는 이유는 React가 최적화를 위해 여러 업데이트를 일괄로 처리하고, 렌더링 성능을 향상시키기 위해 변경된 부분만 업데이트를 적용하기 때문이다.
setState((prevState) => prevState + 1)와 같이 콜백 함수를 사용하면 React는 이전 상태를 기반으로 업데이트를 수행하므로 이런 스케줄링 문제를 피할 수 있다. 콜백 함수는 이전 상태에 안전하게 접근할 수 있도록 보장하며, 이를 통해 비동기 업데이트에 대한 안정성을 제공한다.
이때, 콜백 함수를 사용하는 것은 여전히 비동기적인 방식으로 동작한다. 콜백 함수를 사용하는 것은 React가 상태 업데이트를 처리하는 방식을 변경하는 것이 아니라, 안전한 이전 상태에 접근할 수 있는 방법을 제공하는 것이다.
state를 업데이트하는 함수에 그냥 전달하게 되면 바로 상태가 바뀌지 않기 때문에 이전 state가 가장 최신 state라는 것과 항상 계획된 state 업데이트를 염두에 두고 있다는 것을 보장하게된다. 따라서 이 방식은 항상 최신 상태에서 작업하도록 하는 안전한 방법이 된다.
set 함수 안에 또다른 함수를 사용하게 되면 첫 번째 arguments는 항상 바로 전 값을 가리킨다.
만약 상위 컴포넌트에서 모든 데이터를 관리하고 있고 하위 컴포넌트에서 데이터를 입력 받아 생성하게 된다면 데이터를 관리하는 상위 컴포넌트로 데이터를 이동시키는게 좋다.
function 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),
},
]
return (
<div>
<Expenses expenses={expenses} />
</div>
)
}
자식 컴포넌트에서 부모 컴포넌트로 데이터를 보내는 방식은 state 끌어올리기 개념과 관련이 있다.

컴포넌트 트리가 위와 같이 형성되어 있을 때 NewExpense 컴포넌트로부터 생성한 데이터를 Expenses 컴포넌트에서 활용해야해서 데이터를 곧바로 넘겨주고 싶지만 이런식으로는 동작하지 않는다. 왜냐하면 두 컴포넌트는 직접적으로 연결되어 있지 않기 때문이다. 부모에서 자식으로, 자식에서 부모로만 소통할 수 있다.

이런 경우에는 가장 가까운 부모 컴포넌트를 활용하게 되는데 부모 컴포넌트로 state를 끌어올려서 props로 해당 데이터를 전달하면 된다.

항상 루트 App 컴포넌트까지 상태를 끌어올려야 하는 것은 아니며 컴포넌트 트리에서 데이터를 생성하는 컴포넌트와 데이터가 필요한 컴포넌트 이 두 컴포넌트에 접근할 수 있는 컴포넌트까지만 끌어올리면 된다.
function Expenses(props) {
const filterChangeHandler = (selectedYear) => {
setFilteredYear(selectedYear)
}
return (
<ExpenseFilter
// 부모 컴포넌트에서 함수를 생성해 자식 컴포넌트로 전달하고
onChangeFilter={filterChangeHandler}
/>
)
}
import React from 'react'
import './ExpenseFilter.css'
const ExpensesFilter = (props) => {
const dropdownChangeHandler = (e) => {
// 전달된 함수를 인수와 함께 실행하면
// 자식 컴포넌트의 데이터를 부모 컴포넌트로 전달할 수 있다.
props.onChangeFilter(e.target.value)
}
return (
<select onChange={dropdownChangeHandler}>
<option value="">select year!</option>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
</select>
)
}
export default ExpensesFilter