오늘은 지난번에 작성했던 React 데이터 흐름에 대해 더 알아보자!
앞서 정의한 데이터를 기반으로 위치를 정해보면... "전체 트윗 목록" 상태는 어디에 위치하는 것이 좋을까?
만약 새 글을 추가하는 경우 어떻게 해야 할까?
즉, 두 컴포넌트 모두 트윗 목록에 의존한해야 한다.
그렇다면 두 컴포넌트의 부모는 무엇일까?
바로 Twitter! 전체 트윗 목록 상태는 여기에 위치시켜야 한다
그럼 이제 "사용자가 작성 중인 새로운 트윗 내용"이라는 상태를 위치해보자!
- NewTweetForm에서는 사용자가 트윗 내용을 작성할 수 있다.
- 사용자 입력에 따라 값이 변하므로 "작성 중인 트윗 내용"은 상태이다.
그럼 Tweets나 다른 컴포넌트가 작성중인 내용을 가질 필요가 있나?
NewTweetForm은 그저 버튼이 눌린 후 완성된 하나의 트윗 객체를 전체 트윗 목록에 전달하기만 하면 된다.
입력에 따라 실시간으로 다른 컴포넌트가 변한다면 모를까, 여기에서는 그렇지 않으므로 다른 컴포넌트와 공유할 필요가 없기 때문에 "작성 중인 트윗 내용"이라는 상태는 NewTweetForm에 두는 것으로 충분하다.
흠... 앞서 react는 단방향 흐름을 갖고 있다고 했는데... 값자기 여기서 왜 역방향 데이터 흐름이라는 이야기가 나올까??
상태 위치를 전부 정하고 나서 생각해보니, 부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 것을 발견할 수 있을 것이다.
바로 새로운 트윗 추가가 대표적인 예이다! 버튼을 통한 이 액션은, 부모의 상태를 변화시켜야 한다.
즉, 하위 컴포넌트(NewTweetForm)에서의 클릭 이벤트가 부모의 상태를 바꾸어야만 하는 상황이 온 것이다.
흠.. 그럼 이 문제를 어떻게 해결할 수 있을까?
이를 해결할 수 있는 키워드는 바로 "State 끌어올리기(Lifting state up)"이다.
상태 끌어올리기를 통해 상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있다.
그렇다면 여기서 말하는 상태 끌어올리기라는 것은 무엇일까?
단방향 데이터 흐름이라는 원칙에 따라, 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있다.
하지만 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못한다.
그러므로 하위 컴포넌트에서의 어떤 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 마치 "역방향 데이터 흐름"과 같이 조금 이상하게 들릴 수 있다.
React가 제시하는 해결책은 다음과 같다.
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다
여전히 단방향 데이터 흐름의 원칙에 부합하는 해결 방법. 바로 이것이 "상태 끌어올리기" 이다!
예제 앱은 부모와 자식 컴포넌트가 하나씩 존재하는 트리 구조이다. 그리고, 상태를 변경시킬 수 있는 메서드가 존재한다고 생각해 보자. 다음과 같은 구성을 가지게 될 것이다!
import React, { useState } from "react";
export default function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = () => {
setValue("보여줄게 완전히 달라진 값");
};
return (
<div>
<div>값은 {value} 입니다</div>
<ChildComponent />
</div>
);
}
function ChildComponent() {
const handleClick = () => {
// 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
};
return <button onClick={handleClick}>값 변경</button>;
}
콜백(callback)은 다른 함수(고차 함수)의 인자로 전달되는 함수를 의미한다
// 고차함수
function each(array, iterator) {
for(let i = 0; i < array.length; i++) {
let element = array[i]
iterator(element, i, array)
}
}
// 콜백 함수
function printElement(element) {
console.log(element)
}
each(['hello', 'world'], printElement);
앞서 React의 해결책은 다음과 같다고 말했다
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다
그럼 상태를 변경하는 함수는 무엇일까?
하위 컴포넌트로는 어떻게 전달할 수 있을까?
handleChangeValue
handleButtonClick
이라고 지어줘야 한다.function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = () => {
setValue("보여줄게 완전히 달라진 값");
};
return (
<div>
<div>값은 {value} 입니다</div>
<ChildComponent handleButtonClick={handleChangeValue} />
</div>);
}
<ChildComponent>
는 마치 고차 함수가 인자로 받은 함수를 실행하듯, props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다.
"상태 변경 함수"는 버튼이 클릭할 때 실행되기를 원하므로, 해당 부분에 콜백 함수를 실행하자!
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
// Q. 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
// A. 인자로 받은 상태 변경 함수를 실행하자!
handleButtonClick()
}
return (
<button onClick={handleClick}>값 변경</button>)
}
필요에 따라 설정할 값을 콜백 함수의 인자로 넘길 수도 있다!
function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = (newValue) => {
setValue(newValue);
};
// ...생략...
}
function ChildComponent({ 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}**/>
<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
};
**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;