state를 활용한 동적 페이지
- 상태를 선언한다.
- 리액트의 이벤트와 바인드 한다.
- 모델이 변경됨을 감지한 후, 변경된 부분만 DOM에 반영한다.
## 1. Event
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 컴포넌트: 자바스크립트 함수일 뿐이다.
import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "./Card";
const ExpenseItem = ({ title, amount, date }) => {
const clickHandler = () => {
console.log("Clicked!");
};
return (
// class는 자바스크립트 예약어이기 떄문에 불가능하다.
<Card className="expense-item">
<ExpenseDate date={date} />
<div className="expense-item__description">
<h2>{title}</h2>
<div className="expense-item__price">${amount}</div>
</div>
{/* 이벤트 수신: 리액트는 모든 기본 이벤트를 on으로 시작하는 props로 수신할 수 있다. */}
<button onClick={clickHandler}>Change Title</button>
</Card>
);
};
export default ExpenseItem;
- 이벤트를 수신함으로서, 유저의 행동에 따라 웹사이트가 변하는 동적인 웹사이트를 구현할 수 있다.
- 이벤트에 따른 리랜더링(화면에서의 컴포넌트 변경)을 위해서는 state라는 개념이 필요하다.
- 변화하는 데이터가 사용자 인터페이스에 반영하려면 state가 필요하다.
반응성 추가
- 특정 컴포넌트가 재평가 되어야 한다고 리액트에게 알린다.
- 일반적인 변수는 값이 변경되어도 재평가를 유발하지 않는다.
- 변화하는 데이터가 사용자 인터페이스에 반영하려면 state가 필요하다.
2. useState
컴포넌트가 다시 호출 되는 곳에서 변경된 값을 반영하기 위해 state로 값을 정의할 수 있게 하는 함수이다.
외부에서 데이터를 전달 받는 pops의 경우 read-only이다. 함수형 컴포넌트는 자체적으로 상태(변경 가능한 데이터)를 가지지 못하기 때문에 useState를 활용하여 state를 구현한다.
- state는 컴포넌트가 소유한 로컬 데이터로 적용 범위는 현재 컴포넌트에 한정된다.
1) 기본 개념
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { useState } from "react";
import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "./Card";
const ExpenseItem = (props) => {
// const [변수, 업데이트 함수] = useState(기본값)
const [title, setTitle] = useState(props.title);
// 클릭시, 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>
</Card>
);
};
export default ExpenseItem;
- 초기값은 초기 렌더링 시에만 사용된다. 이후 다시 랜더링 될 때는 이 값은 무시된다.
useState()
함수는 두 가지를 반환한다.- state
- state를 업데이트 하는 함수:
예측가능성
- 특정 함수로만 state를 업데이트할 수 있는 이유는 변경을 예측(추적)할 수 있다. => 데이터 관리를 보다 쉽게 만든다.
- 등호 연산자로 state를 변경하지 않기 때문에
const
를 사용해도 괜찮다.
리랜더링 과정
리액트는 실제 DOM이 아닌, 가상 DOM을 통해 선언된 상태를 비교 분석하여 UI를 업데이트한다.
이전/ 현재 가상 DOM 트리를 비교하는 재조정 알고리즘을 통해 변경된 트리의 요소를 감지하여 해당 요소만 업데이트하여 DOM에 반영한다.
- 위의 예시의 경우, 클릭 시 해당 state를 변경하는 함수를 실행하고 이는 JSX 코드를 다시 평가한다.
- 지난번과 비교하여 감지된 변화를 화면에 랜더한다.
2) 데이터 관리
state는 데이터를 저장하기 위해서 사용될 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState } from "react";
import "./ExpenseForm.css";
const ExpenseForm = () => {
// 데이터를 저장하기 위해, useState를 사용할 수도 있다.
// 컴포넌트 당 별도의 상태를 갖고 각각 업데이트하고 관리할 수 있다.
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 (
// 생략
);
};
export default ExpenseForm;
- 하나의 컴포넌트가 여러 개의 state를 가질 수 있다.
- 그러나 객체를 사용하여 하나의 state로 관리할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { useState } from "react";
import "./ExpenseForm.css";
const ExpenseForm = () => {
const [userInput, setUserInput] = useState({
enteredTitle: "",
enteredAmount: "",
enteredDate: "",
});
// 이전 state를 사용한다면, 아래와 같은 방식으로 사용해야 한다.
const titleChangeHandler = (event) => {
setUserInput((prevState) => {
return {
...prevState,
enteredTitle: event.target.value,
};
});
};
// 스프레드 연산자와 오버라이드를 통해 useInput의 특정 키만 업데이트
// 그러나 이전 state가 최신 업데이트가 되지 않아 오류가 발생할 수도 있다.
// 위의 방식이 더 좋다.
const amountChangeHandler = (event) => {
setUserInput({
...userInput,
enteredAmount: event.target.value,
});
};
const dateChangeHandler = (event) => {
setUserInput({
...userInput,
enteredDate: event.target.value,
});
};
return (
// 생략
);
};
export default ExpenseForm;
- state 업데이트가 이전 state에 의존하고 있다면, 업데이트가 오랫동안 되지 않은 state를 사용하지 않도록 해야 한다.
3) 양방향 바인딩
state를 사용하여 양방향 바인딩을 구현할 수 있다.
변경되는 입력 값만 수신하는 것이 아니라 입력에 새로운 값을 다시 전달할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import React, { useState } from "react";
import "./ExpenseForm.css";
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);
};
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
console.log(expenseData);
// 리셋하기
setEnteredTitle("");
setEnteredAmount("");
setEnteredDate("");
};
return (
<form className="new-expense__controls" onSubmit={submitHandler}>
<div className="new-expense__control">
<label>Title</label>
<input
type="text"
// 양방향 바인딩
value={enteredTitle}
onChange={titleChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Amount</label>
<input
type="number"
min="0.01"
step="0.01"
// 양방향 바인딩
value={enteredAmount}
onChange={amountChangeHandler}
/>
</div>
<div className="new-expense__control">
<label>Date</label>
<input
type="date"
min="2019-01-01"
max="2022-12-31"
// 양방향 바인딩
value={enteredDate}
onChange={dateChangeHandler}
/>
</div>
<div className="new-expense__actions">
<button type="submit">Add Expense</button>
</div>
</form>
);
};
export default ExpenseForm;
- input의 value 속성을 state와 연결하여 양방향으로 바인딩할 수 있다.
3. Emit (데이터-끌어올리기)
props와의 반대 개념으로 자식이 부모에게 데이터를 전달한다.
1) 부모 컴포넌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import "./NewExpense.css";
import ExpenseForm from "./ExpenseForm";
const NewExpense = (props) => {
const saveExpenseDataHandler = (enteredExpenseData) => {
const expenseData = {
...enteredExpenseData,
id: Math.random().toString(),
};
// App.vue에게 emit
props.onAddExpense(expenseData);
};
return (
<div className="new-expense">
{/* onSaveExpenseData라는 이벤트를 통해 자식에게 데이터를 받는다. */}
<ExpenseForm onSaveExpenseData={saveExpenseDataHandler} />
</div>
);
};
export default NewExpense;
onSaveExpenseData
함수의 매개변수를 통해 데이터를 받아서 저장한다.- App 컴포넌트(최상위 컴포넌트)로 데이터를 끌어올린다.
- 형제 컴포넌트들도 데이터를 사용하기 위해서는 결국 App.vue까지 데이터를 끌어올려야 한다.
- App컴포넌트에 데이터가 있으면, props를 통해 다른 컴포넌트들에게 데이터를 전달하기 용이하다.
2) 자식 컴포넌트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { useState } from "react";
import "./ExpenseForm.css";
const ExpenseForm = (props) => {
// 생략
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
// 데이터를 부모에게 emit
props.onSaveExpenseData(expenseData);
setEnteredTitle("");
setEnteredAmount("");
setEnteredDate("");
};
return (
<form className="new-expense__controls" onSubmit={submitHandler}>
{/* 생략 */}
</form>
);
};
export default ExpenseForm;
- props를 사용해서 부모 컴포넌트로부터 함수를 받고 자식 컴포넌트에서 그 함수를 통해 데이터를 보낸다.