//ExpandItem.js
import "./ExpendItem.css";
import ExpendDate from "./ExpendDate";
import Card from "./Card";
function ExpendItem(props) {
const clickHandler = () => {
console.log("clicked!");
};
return (
<Card className="expense-item">
<ExpendDate date={props.date} />
<div className="expense-item__description">
<h2>{props.title}</h2>
<div className="expense-item__price">${props.amount}</div>
<button onClick={clickHandler}>click</button>
</div>
</Card>
);
}
export default ExpendItem;
처음에 살펴봤듯, React의 기본 동작은 자바스크립트코드로 수행된다. 따라서 App.js라는 파일이 수행되고나면 다시 렌더링 되지 않는다.
이 말을 왜 하느냐면 다음 eventHandler 버튼을 통해서 글자를 바꾸고 싶기 때문이다.
console.log();
를 통해서 출력을 해보면 title은 잘 바뀌고 있다.import "./ExpendItem.css";
import ExpendDate from "./ExpendDate";
import Card from "./Card";
function ExpendItem(props) {
let title = props.title;
const clickHandler = () => {
title = "clicked!";
console.log(title);
};
return (
<Card className="expense-item">
<ExpendDate date={props.date} />
<div className="expense-item__description">
<h2>{title}</h2>
<div className="expense-item__price">${props.amount}</div>
<button onClick={clickHandler}>Change Title</button>
</div>
</Card>
);
}
export default ExpendItem;
3에서 살펴봤던 렌더링이 안되는 문제는 useState라는 React Hook을 통해서 해결 할 수 있다. useState안에 변수는 항상 업데이트가 되도록 관리된다.
import React, { useState } from "react";
처음 useState를 import
하는것으로부터 시작된다. 다음 예제를 살펴보자function ExpendItem(props) {
let arr = ["a", "b", "c", "d", "e", "f"];
let [title, setTitle] = useState(arr[0]);
const clickHandler = () => {
let index = (arr.indexOf(title) + 1) % arr.length;
setTitle(arr[index]);
console.log(title);
};
return (
<Card className="expense-item">
<ExpendDate date={props.date} />
<div className="expense-item__description">
<h2>{title}</h2>
<div className="expense-item__price">${props.amount}</div>
<button onClick={clickHandler}>Change Title</button>
</div>
</Card>
);
}
export default ExpendItem;
let [title, setTitle] = useState(arr[0]);
function ExpendItem(props) {
let [title, setTitle] = useState(props.title);
const clickHandler = () => {
setTitle("Updated");
console.log(title);
};
return (
<Card className="expense-item">
<ExpendDate date={props.date} />
<div className="expense-item__description">
<h2>{title}</h2>
<div className="expense-item__price">${props.amount}</div>
<button onClick={clickHandler}>Change Title</button>
</div>
</Card>
);
}
여태까지 만들었던 페이지는 매우 정적이므로, 새로운 데이터를 입력받아 목록에 추가하려고 한다. 그러기 위해서 form을 만든다.
//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;
//NewExpense.js
import react from "react";
import "./NewExpense.css";
import ExpenseForm from "./ExpenseForm";
const NewExpense = () => {
return (
<div className="new-expense">
<ExpenseForm />
</div>
);
};
export default NewExpense;
이벤트리스너의 기능을 테스트 하기 위해서 입력 창에 바뀌는 정보는 콘솔에 출력해보려고 한다.
먼저 바뀌는 값을 콘솔에 출력하는 함수를 만든다.
const titleChangeHandler = (event) => {
console.log(event.target.value);
};
그리고 폼의 인풋박스에다가 추가해준다.
import react from "react";
import "./ExpenseForm.css";
const ExpenseForm = () => {
const titleChangeHandler = (event) => {
console.log(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" />
</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;
한 컴포넌트 안에 여러개의 State를 다룰 수 있다.
let [enteredTitle, setEnteredTitle] = useState("");
let [enteredAmount, setEnteredAmount] = useState("");
let [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를 객체로서 한번에 다루는 방법도 있다.
import react, { useState } from "react";
import "./ExpenseForm.css";
const ExpenseForm = () => {
const [userState, setUserState] = useState({
enteredTitle: "",
enteredAmount: "",
enteredDate: "",
});
const titleChangeHandler = (event) => {
setUserState({
...userState,
enteredTitle: event.target.value,
});
};
const amountChangeHandler = (event) => {
setUserState({
...userState,
enteredTitle: event.target.value,
});
};
const dateChangeHandler = (event) => {
setUserState({
...userState,
enteredTitle: 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>
);
};
export default ExpenseForm;
setUserState({
...userState,
enteredTitle: event.target.value,
});
setUserState((prevState) => {
return { ...prevState, enteredTitle: event.target.value }
});
의문점 : 저렇게 해야하는 이유, anonymous function 이해x
다시 useState는 예전방식으로 돌아와서 form이 submit 되었을 때 이벤트를 다루는 방법에 대해서 알아보자
<form onSubmit={submitHandler}>
form이 submit 처리 되었을 때 수행할 함수를 선언한다.
submit 테스트 하기
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
console.log(expenseData);
};
form이 submit되는 것을
event.preventDefault();
로 막았다. 그렇다면Add Expense
버튼을 눌렀을 때 input box가 초기화 되기 위해선 어떻게 해야할까?
submitHandler
함수에서setEntered***('')
를 이용해야 할 것이다. 그런데 또 알아야 할것이setEntered***('')
함수는entered***
변수의 state를 관리하는것인데 input box에는 변수가 없다.
따라서 inputbox의 value를 변수로 지정해줘야한다.
const submitHandler = (event) => {
event.preventDefault();
const expenseData = {
title: enteredTitle,
amount: enteredAmount,
date: new Date(enteredDate),
};
console.log(expenseData);
setEnteredTitle("");
setEnteredAmount("");
setEnteredDate("");
};
<input
type="text"
value={enteredTitle}
onChange={titleChangeHandler}
/>
부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때에는
props
를 이용하면 됐다. 그런데 이 프로젝트에서 볼수 있듯이ExpenseForm.js
에서 받은 정보는App.js
가 알 수 있어야expenses
배열에 저장할 수 있을 것이다.
자식 컴포넌트가 가지고 있는 정보는 어떻게 부모 컴포넌트로 옮길까?
먼저
ExpenseForm
에서NewExpense
로 옮겨야 한다. 과정은 다음과 같다.
NewExpense.js
에서 자식 컴포넌트를 찾는다.<ExpenseForm onClickAddExpense={dataFromFormToNewExpense} />
다음과 같이 props를 추가한다. 추가한 props는 AddExpense
버튼이 눌렸을때 실행되는것은 dataFromFormToNewExpense
함수 라는 뜻이다.dataFromFormToNewExpense
함수는 다음과 같다.const dataFromFormToNewExpense = (prevData) => {
const newExpenseData = {
...prevData,
id: Math.random().toString(),
};
console.log(newExpenseData);
};
newExpenseData
라는 객체를 생성해주고 id
를 추가해준다. 그리고 props
에서 받아온 onClickAddExpense
함수에 만든 객체를 인자로 넣어주고 실행시킨다.Expenses Filter을 만든 후에 데이터를 보내는 연습을 해보자
ExpenseFilter.js
를 만든다.
import React from "react";
import "./ExpenseFilter.css";
const ExpensesFilter = (props) => {
const pickDate = (event) => {
props.onClickDate(event.target.value);
};
return (
<div className="expenses-filter">
<div className="expenses-filter__control">
<label>Filter by year</label>
<select onChange={pickDate} value={props.selectedDate}>
<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 ExpensesFilter;
onChange
와 pickDate
를 이용해서 데이터를 상향식으로 보내주는건 익숙해졌다. const [newDate, setNewDate] = useState("2020");
const DateFromFilterToExpenses = (prevDate) => {
setNewDate(prevDate);
};
<ExpensesFilter
selectedDate={newDate}
onClickDate={DateFromFilterToExpenses}
/>
selectedDate
를 props
로 다시 내려보내주는 일인데, 이것이 필요한 이유는 다음과 같다.Expenses.js
에서 알 수는 없다. 따라서 임의의 값을 useState
로 초기화 해 준후, 그 값을 ExpenseFilter
로 보내서 드롭다운 메뉴의 시작을 초기화한 값으로 해주는 것이다.