지금 내가 진행하고 있는 프로젝트의 컴포넌트의 구조는 위와 같다. 컴포넌트가 부모 계층으로서 그 아래에 와 컴포넌트가 자식 계층에 존재한다.
이때, 리액트는 부모에서 자식으로만 데이터가 흐를 수 있다 이것을 단방향 데이터 흐름이라고 한다. 그렇다면 자식 계층끼리는 데이터를 주고 받을 방법이 없을까?
있다! 바로 부모의 data(state)와 setData(modifier함수)를 이용하는 것이다.
각각 자식 컴포넌트에 data, setData를 props로 전달하고 setData로 인해 변경, 추가, 삭제등 이벤트가 일어났을 때 그것이 다시 부모계층으로 진행됨 -> data가 변경 됐으니 그것을 Props으로 가지는 자식계층은 리렌더링 됨
따라서 이벤트는 역방향으로 일어나고(자식 ->부모) 데이터는 단방향으로만 흐름(부모 -> 자식)
이렇게 공통 부모의 state를 설정해서 자식계층 간 데이터와 이벤트 흐름을 이용해서 문제를 해결하는 방법을 state를 끌어올리기 라고 함.
//App.js
function App() {
const [data, setData] = useState([]); // 설명1
const dataId = useRef(0); // 설명3 참고
const onCreate = ({ author, content, emotion }) => {// 설명2참고
const created_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_date,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
};
return (
<div className="App">
<DiaryEditor onCreate={onCreate} /> // 설명 4참고
<DiaryList diaryList={data} />
</div>
);
}
부모인 app에 [data,setData] = useState([]) 생성. 작성된 다이어리가 없을 때 = 빈배열이어야 하니까 기본값 []
다이어리를 생성해서 추가하는 작업은 DiaryEditor에서 할거니까 거기서 setData()를 실행해줄거임. 그 전에 setData에 작성자와 게시글 내용 등 정보를 넣고 실행시켜줄 함수를 만들어줌 그 함수가 onCreate
. 보면 이 함수의 매개변수로 {author, content, emotion}
이렇게 들어가 있는데, 비 구조화 할당으로 만약 매개변수 자리에 그냥 객체 하나가 통으로 들어오면 (Ex. state = {author : 'jisu', content: 'hi', ..}) 여기서 {author, content} 이런 매개변수라면 저 author를 객체에서 빼서 바로 사용하겠다는거.
그래서 결론적으로는 데이터를 생성하는 DiaryEditor에 프롭으로보내줄 함수니까 DiaryEditor에서 State(생성된 데이터)가 있을거 아닙니까? 걔네를 인수로 사용해서 setData를 호출하라는 함수가 onCreate인거다. setData([newItem, ...data])
이 문장은 setData에 객체들을 배열안에 합쳐서 넣겠다는건데 즉, newItem(새로 생성된 데이터로 만든 객체)과 ...data(기존의 데이터로 만들어진 객체가 담긴 배열들을 spread 연산자로 풀어서 사용하겠다는 말)를 합쳐서 배열에 담아서 업데이트시킴
설명2에서 newItem을 생성할 때, id: dataId.current
가 있는데 이건 새로 생기는 다이어리에 대해서 각각 id를 부여해주는건데,dataId = useRef(0)
으로 dataId.current = 0
이고 newItem을 생성할때마다 dataId.current +=1이 되는거다.
useRef 쓴 이유 : 컴포넌트 내부에서 활용할 변수가 필요할 때 useState로 변수를 선언하게 되면 변수 값의 변경이 컴포넌트의 리렌더를 유발하여 의미없는 리렌더가 발생할 위험이 큼. 따라서 렌더링과 관련없는 변수를 선언할 때에는 useRef를 사용하는것이 바람직.
각각의 state(여기서는 data라는 이름)와 setState(여기서는 setData이고 그것을 onCreate 함수에서 호출시킴)를 자식 계층인 DiaryEditor
와 DiaryList
로 보내준다.
//DiaryEditor
const DiaryEditor = ({ onCreate }) => {
// 현재 맥락과 관계없는 코드는 생략했음
const [state, setState] = useState({
author: "",
content: "",
emotion: 1,
});
const handleChangeState = (e) => {
setState({ ...state, [e.target.name]: e.target.value });
};
const handleSubmit = () => {
// 작성자와 컨텐츠 길이에 따른 input 포커싱 생략
// ...
onCreate(state);
setState({
author: "",
content: "",
emotion: 1,
}); // 현재 state를 빈 값으로 변경하여 초기화
alert("저장 성공!");
};
return (
<div className="DiaryEditor">
<h2>Diary</h2>
...생략
<div>
<button onClick={handleSubmit}>저장하기</button>
</div>
</div>
);
};
여기서 봐야할 코드는 저장하기 버튼 클릭시 호출되는 handleSumbit
함수다.
부모로부터 받은 onCreate 함수에 현재 유저가 입력한 값(author, content, emotion) = state를 인자로 넘겨서 호출한다. 이때 App에서 만든 onCreate = ({ author, content, emotion })
이런 형태이므로 바로 state의 author와 content, emotion을 사용할 수 있다. (이해 안 간다면 비 구조화 할당 검색)
따라서 onCreate의 콜백함수로 들어가 있는 setData 함수에 새롭게 저장된 newItem(App.js 참고) 객체와 기존의 data 객체가 합쳐져서 리스트 데이터가 추가될 수 잇다.
//DiaryList.js
const DiaryList = ({ diaryList }) => {
return (
<div className="DiaryList">
<h2>diary list</h2>
<p>{diaryList.length}개의 diary가 있습니다.</p>
<div>
{diaryList.map((i) => (
<DiaryItem key={i.id} {...i} />
))}
</div>
</div>
);
};
부모 컴포로부터 받은 prop인 diaryList를 이용해서,
(이해가 안 갈까봐 부연 설명하자면 diaryList는 현재
diaryList =
[{author : 'jisu', content: 'hi', emotion : 3}, {author : 'sie'...생략 }]
이런 형태다. 따라서 이 리스트를 map 메서드를 사용해 각각의 객체들에 접근할 수 있는거다. 이때 리스트에 생성될 각각의 item들은 DiaryItem
컴포넌트로 불리했기 때문에 거기서 생성을 하는거고, 이 DiaryList에서는 이렇게 생성된 data를 이용해서 1.생성된 다이어리가 몇 개인지 보여주고 2. 각각의 아이템을 만들 수 있게 배열 안의 객체를 map으로 제공해주면 된다.