1.React 데이터 흐름
학습목표
- React에서의 데이터 흐름, 단방향 데이터 흐름을 이해할 수 있다.
- 어떤 컴포넌트에 state가 위치해야 하는지 알 수 있다.
- State 끌어올리기의 개념을 이해할 수 있다.
- 상태 변경 함수가 정의된 컴포넌트와, 상태 변경 함수를 호출하는 컴포넌트가 다름을 알 수 있다.
1.디자인을 전달 받으면 가장 먼저 컴포넌트 계층 구조를 분석한다.
=> 하나의 기능을 가진 컴포넌트로 구분
2.데이터의 전달 위치 설정한다.
=> 데이터의 흐름(하향식 top-down)을 고려하여 부모 컴포넌트에서 Props 방식으로 데이터 전달
3.state를 비롯한 필요한 데이터 정의 및 구분
=> state는 최소화하는 것이 좋다.
[X] 부모 컴포넌트에서 Props로 전달되는가?
[X] 시간이 지나도 변하지 않는가?
[X] 컴포넌트 안의 다른 state나 props로 계산 가능한가?
-> state가 아닌 일반 데이터
4.state의 위치 결정 , 역방향 데이터 흐름 추가
하나의 state가 여러개의 컴포넌트에 영향을 끼칠 경우, 공통되는 부모 컴포넌트에서 선언하고 영향을 끼치지 않으면 사용하는 컴포넌트에서 선언
=> 작성중인 상태를 나타내는 state는 새로운 글을 작성하는 컴포넌트에서 정의하여 사용.
(새로운 트윗 추가 -> 부모가 가진 전체트윗목록(state)상태를 변화시킴
즉 하위 컴포넌트(NewTweetForm)에서의 클릭 이벤트가, 부모의 상태를 바꾸어야만 하는 상황)
반면 게시글 목록을 표현하는 state는 새글을 추가하는 컴포넌트의 영향을 받으므로 게시글 목록 컴포넌트보다 공통적인 부모 컴포넌트에서 state를 정의하여 사용.
1-2. State 끌어올리기
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다.
props로 state 대신 '상태변경함수 Handler' 를 전달
콜백 함수처럼 함수 자체를 props로 전달 → 하위 컴포넌트가 실행
=> 하위 컴포넌트에서 전달받은 상태변경함수를 실행하면 state 변경되며 단반향 데이터 흐름 원칙도 유지
예제 1
<부모 컴포넌트>
function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
// 상태를 변경하는 함수
const handleChangeValue = () => {
setValue("보여줄게 완전히 달라진 값");
};
{/* 설정할 값을 콜백 함수의 인자로 넘길 수도 있음
const handleChangeValue = (newValue) => {
setValue(newValue);
};
*/}
return (
<div>
<div>값은 {value} 입니다</div>
{/* props를 이용하여 상태 변경 함수를 하위 컴포넌트로 전달 */}
<ChildComponent handleButtonClick={handleChangeValue} />
</div>
);
}
<자식 컴포넌트>
// 인자로 받은 상태 변경 함수를 실행
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
handleButtonClick()
};
{/* 설정할 값을 콜백 함수의 인자로 넘길 수도 있음
const handleClick = () => {
handleButtonClick('넘겨줄게 자식이 원하는 값')
}
*/}
return <button onClick={handleClick}>값 변경</button>;
}
import React, { useState } from "react";
import "./styles.css";
const currentUser = "김코딩";
function Twittler() {
const [tweets, setTweets] = useState([
{
uuid: 1,
writer: "김코딩",
date: "2020-10-10",
content: "안녕 리액트"
},
{
uuid: 2,
writer: "박해커",
date: "2020-10-12",
content: "좋아 코드스테이츠!"
}
]);
const addNewTweet = (newTweet) => {
setTweets([...tweets, newTweet]);
}; // 이 상태 변경 함수가 NewTweetForm에 의해 실행되어야 합니다.
return (
<div>
<div>작성자: {currentUser}</div>
<NewTweetForm onButtonClick={addNewTweet}/> // props로 전달
<ul id="tweets">
{tweets.map((t) => (
<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
{t.content}
</SingleTweet>
))}
</ul>
</div>
);
}
<자식 컴포넌트>
function NewTweetForm({ onButtonClick }) {
const [newTweetContent, setNewTweetContent] = useState("");
const onTextChange = (e) => {
setNewTweetContent(e.target.value);
};
const onClickSubmit = () => {
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent
};
// TDOO: 여기서 newTweet이 addNewTweet에 전달되어야 합니다.
onButtonClick(newTweet);
};
return (
<div id="writing-area">
<textarea id="new-tweet-content" onChange={onTextChange}></textarea>
<button id="submit-new-tweet" onClick={onClickSubmit}>
새 글 쓰기
</button>
</div>
);
}
<자식 컴포넌트>
function SingleTweet({ writer, date, children }) {
return (
<li className="tweet">
<div className="writer">{writer}</div>
<div className="date">{date}</div>
<div>{children}</div>
</li>
);
}
export default Twittler;
2. Effect Hook
학습 목표
- Side effect가 어떤 의미인지 알 수 있다.
- React 컴포넌트를 만들 때 side effect로부터 분리해서 생각할 수 있다. (비즈니스 로직과 표현 영역 구분)
- Side effect의 예를 들 수 있다.
- Effect Hook을 이용해 비동기 호출 및 AJAX 요청과 같은 side effect를 React 컴포넌트 내에서 처리할 수 있다.
- Effect Hook에서의 dependency array 사용법을 이해할 수 있다.
- 컴포넌트 내에서 네트워크 요청 시, 로딩 화면과 같이 보다 나은 UI를 만드는 법을 이해할 수 있다.
2-1. Side Effect
let foo = 'hello';
function bar() {
foo = 'world';
}
bar(); // bar는 Side Effect를 발생시킵니다!
function upper(str) {
return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않습니다 (Immutable)
}
upper('hello') // 'HELLO'
2-2. Effect Hook 기본
2-3. Effect Hook 조건부 실행
useEffect(()=>{
console.log(몇 번 호출될까요?)
})
컴포넌트가 처음 생성되거나, props가 업데이트 되거나, state가 업데이트 될 때마다 실행
useEffect(()=>{
console.log(몇 번 호출될까요?)
}, [])
컴포넌트가 처음 생성될 때만 단 한 번만, effect 함수가 실행
대표적으로 처음 단 한 번, 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할수있다.
useEffect(()=>{
console.log(몇 번 호출될까요?)
}, [count])
컴포넌트가 최초 마운트될때(화면에 나타날때), state인 count가 업데이트 될 때마다 실행
2-4. 컴포넌트 내에서의 Ajax 요청
// 컴포넌트 내에서 필터링
// 처음 단 한 번, 외부 API로부터 명언 목록을 받아오고, filter 함수를 이용
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";
export default function App() {
const [proverbs, setProverbs] = useState([]);
const [filter, setFilter] = useState("");
useEffect(() => {
console.log("언제 effect 함수가 불릴까요?");
const result = getProverbs();
setProverbs(result);
}, []);
const handleChange = (e) => {
setFilter(e.target.value);
};
return (
<div className="App">
필터
<input type="text" value={filter} onChange={handleChange} />
<ul>
{proverbs
.filter((prvb) => {
return prvb.toLowerCase().includes(filter.toLowerCase());
})
.map((prvb, i) => (
<Proverb saying={prvb} key={i} />
))}
</ul>
</div>
);
}
function Proverb({ saying }) {
return <li>{saying}</li>;
}
//LocalStorage API를 이용
// storageUtil.js
localStorage.setItem(
"proverbs",
JSON.stringify([
"좌절감으로 배움을 늦추지 마라",
"Stay hungry, Stay foolish",
"Memento Mori",
"Carpe diem",
"배움에는 끝이 없다"
])
);
export function getProverbs(filterBy = "") {
const json = localStorage.getItem("proverbs");
const proverbs = JSON.parse(json) || [];
return proverbs.filter((prvb) =>
prvb.toLowerCase().includes(filterBy.toLowerCase())
);
}
// 컴포넌트 외부에서 필터링
// 검색어가 바뀔 때마다, 외부 API를 호출
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";
export default function App() {
const [proverbs, setProverbs] = useState([]);
const [filter, setFilter] = useState("");
const [count, setCount] = useState(0);
useEffect(() => {
console.log("언제 effect 함수가 불릴까요?");
const result = getProverbs(filter);
setProverbs(result);
}, [filter]);
const handleChange = (e) => {
setFilter(e.target.value);
};
const handleCounterClick = () => {
setCount(count + 1);
};
return (
<div className="App">
필터
<input type="text" value={filter} onChange={handleChange} />
<ul>
{proverbs.map((prvb, i) => (
<Proverb saying={prvb} key={i} />
))}
</ul>
<button onClick={handleCounterClick}>카운터 값: {count}</button>
</div>
);
}
function Proverb({ saying }) {
return <li>{saying}</li>;
}
//LocalStorage API를 이용
// storageUtil.js
/*setItem() :localStorage에 아이템을 추가하기 위해서는 setItem() 함수를 사용
window.localStorage.setItem(key, value)*/
localStorage에 아이템을 추가하기 위해서는 setItem() 함수를 사용합니다.
localStorage.setItem(
"proverbs",
JSON.stringify([
"좌절감으로 배움을 늦추지 마라",
"Stay hungry, Stay foolish",
"Memento Mori",
"Carpe diem",
"배움에는 끝이 없다"
])
);
export function getProverbs(filterBy = "") {
const json = localStorage.getItem("proverbs");
/*getItem() :localStorage의 아이템을 읽기 위해서는 getItem() 함수를 사용
window.localStorage.getItem(key)*/
const proverbs = JSON.parse(json) || [];
return proverbs.filter((prvb) =>
prvb.toLowerCase().includes(filterBy.toLowerCase())
);
}
=> 두 방식의 차이점
http://서버주소/proverbs
라고 가정//fetch API를 써서 서버에 요청
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
});
}, [filter]);
//fetch API를 써서 서버에 요청
//나머지 생략하고 로딩 화면이랑 에러 화면 구현
const [loading, setLoading] = useState(false);
//처음에는 로딩 안됀 상태로 시작
const [error, setError] = useState(undefined);
//처음에는 에러가 없는 상태
//그냥 useState()해도 처음에는 undefined할당
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
})
.catch((e) => setError("에러가 발생했음"))
.finally(() => setLoading(false));
//데이터를 받아왔던 안받아왔던 로딩은 끝을 내야해
}, [filter]);
if (loading) return <p>loading..</p>;
if (error) return <p>{error}</p>;