Unidirectional Data Flow
자기 자신의 상태를(local state) 가지는 많은 컴포넌트가 존재하게 되면 앱을 관리하기가 어려워진다. 2개 이상의 컴포넌트에서 공유해야 할 state가 있다면 application state로 정의하고 props로 데이터를 전달 받는게 관리하기 편하다. 이 예에서 player의 이름, id 가 그 예이다. 그 데이터는 최상위 컴포넌트인 App 컴포넌트에서 아래방향으로 흐른다. 이것을 Unidirectional Data Flow 라고 한다. Application state에서 값을 변경하면 변경된 값이 아래 컴포넌트 트리의 아래방향으로 흘러서 변경된 값이 모두 적용된다. 하지만 read only 이기 때문에 자식이 이 값을 바꾸어서는 안된다.바꾼다 하더라도 바뀐 값이 부모에게까지 변경된 값이 적용되지 않는다. 비록 자식으로는 변경된 값이 적용되겠지만,,,
Lifting state Up
앞에서 우리는 Counter 컴포넌트에 적용된 local state 인 score를 다루었다. 이 local 상태 정보는 다른 컴포넌트와 값을 공유 할 수 없는 해당 컴포넌트에서만 상태정보를 유지할 수 있다. 만일 player들 중 가장 높은 score를 기록한 Player를 찾고 싶다면 누구인지 찾을수 있을까? Counter 컴포넌트가 이 score를 갖고 있으면 high score를 찾을수 없다. 이 score를 부모인 App 컴포넌트가 갖고 있어야만 가능하다. local state를 다른 컴포넌트와 공유하기 위해서 부모컴포넌트에 state를 관리하는 것을 lifting up 이라고 한다.
App 컴포넌트에 score를 state에 기록하고(L4 ~ L7) 바로 아래 컴포넌트인 Player 컴포넌트에 props로 내려준다. (L27, L28)
App.jsx
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 |
class App extends React.Component { state = { players: [ {name: 'LDK', score: 0, id: 1}, {name: 'HONG', score: 0, id: 2}, {name: 'KIM', score: 0, id: 3}, {name: 'PARK', score: 0, id: 4}, ] }; handleRemovePlayer = (id) => { this.setState(prevState => { return { players: prevState.players.filter(item => item.id !== id) } }) } render() { return ( <div className="container p-3"> <Header title="My scoreboard" totalPlayers={this.state.players.length} /> {/*Players List*/} { this.state.players.map(item => <Player key={item.id} name={item.name} score={item.score} removePlayer={this.handleRemovePlayer} id={item.id} />) } </div> ); } } |
Player 컴포넌트에서는 props로 받은 score를 다시 Counter 컴포넌트에 내려준다.
components/Player.jsx
1 2 3 |
... <Counter score={props.score}></Counter> ... |
Counter 컴포넌트에서는 local state를 삭제하고 props로 score를 받는다. 먼저, state 변수와 함수를 모두 삭제하고 render 함수만 남겨둔다.
components/Counter.jsx
1 2 3 4 5 6 7 8 9 10 11 |
export class Counter extends React.Component { render() { return ( <div className='d-flex justify-content-between align-items-center'> <button className='btn btn-info'> - </button> <span>{this.props.score}</span> <button className='btn btn-info'> + </button> </div> ); } } |
Counter 컴포넌트는 더이상 stateful 하지 않으므로 class 컴포넌트가 아닌 function 컴포넌트로 바꾸는게 더 효율적이다.
1 2 3 4 5 6 7 8 9 10 |
export const Counter = (props) => { return ( <div className='d-flex justify-content-between align-items-center'> <button className='btn btn-info'> - </button> <span>{props.score}</span> <button className='btn btn-info'> + </button> </div> ); } |
Communicating between components
아직 Counter 컴포넌트에 + – 버튼이 작동하지 않는다. local state일때는 이벤트를 발생하여 state를 바꾸면 되었지만 지금은 props 받아서 read only이기 때문에 + – 이벤트가 발생하면 그 이벤트를 state를 가지고 있는 부모까지 전달하여 부모가 그 값을 바꾸면 바뀐 값이 아래로 흘러서 Counter 컴포넌트까지 바뀐 값이 적용된다. 여기서는 App => Player => Counter로 데이터를 흐르게 한다.
먼저 App 컴포넌트에 handleChangeScore 이벤트 처리 함수를 추가한다.(L4 ~ L6) 로직을 추가하기전에 어떻게 값들이 통신하는지 알기 위해서 콘솔창에 로그만 찍어본다. 첫번째 파라메터는 배열 인덱스이고 두번째 파라메터는 감소는 -1 증가는 1을 받는다. Player 컴포넌트에는 index와 changeScore 속성으로 handleChangeScore 함수 포인터를 넘긴다.
changeScore 속성으로 함수를 속성으로 넘긴다(L18)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class App extends React.Component { ... handleChangeScore = (id, delta) => { console.log('id: ' + id, 'delta: ' + delta); } render() { return ( <div className="container p-3"> <Header title="My scoreboard" totalPlayers={this.state.players.length} /> {/*Players List*/} { this.state.players.map(item => <Player key={item.id} name={item.name} score={item.score} id={item.id} removePlayer={this.handleRemovePlayer} changeScore={this.handleChangeScore} />) } </div> ); } } |
Player 컴포넌트에는 props로 받은 id와 changeScore 함수 포인트를 다시 Counter 컴포넌트에 넘긴다.
components/Player.jsx
1 |
<Counter score={props.score} id={props.id} changeScore={props.changeScore}></Counter> |
Counter 컴포넌트에는 클릭 이벤트를 추가해서 index와 증가/감소를 나타내는 두개의 파라메터를 부모로 올려준다.
components/Counter.jsx
1 2 3 4 5 6 7 8 9 |
export const Counter = (props) => { return ( <div className='d-flex justify-content-between align-items-center'> <button className='btn btn-info' onClick={() => props.changeScore(props.id, -1)}> - </button> <span>{props.score}</span> <button className='btn btn-info' onClick={() => props.changeScore(props.id, 1)}> + </button> </div> ); } |
증가, 감소 로직
forEach로 순환하는것은 immutable 하지 않다. 즉, 원본 배열을 수정하므로 이 forEach 작업전에 먼저 스프레드 연산자를 사용해서 deep copy를 수행한다.
App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
handleChangeScore = (id, delta) => { console.log('id: ' + id, 'delta: ' + delta); this.setState(prevState => { // 새로운 players 배열 생성 const players = [ ...prevState.players ]; players.forEach(player => { if (player.id === id) { player.score += delta; } }) return { players } }) } |
요약
카운터 컴포넌트에 score 를 local state 로 갖게 되면 전체 스코어 합계라던가 가장 높은 스코어, 가장 낮은 스코어 같은 로직을 구현할 수 없다. 이것을 하기 위해서는 score 를 가장 상위 컴포넌트로 올려야 하는데 이것을 Lifting Up 이라고 하고 score를 가장 상위 컴포넌트에 application state로 관리하고 이 score 를 자식 컴포넌트에 props 로 내려준다. 그러면 자식 컴포넌트는 score 가 props로 내려왔기 때문에 read only 이기 때문에 직접 수정할 수 없고 수정 요청을 부모에게 보내게 된다. 만일 부모가 하나가 아니라 두개 이상이라면 수정요청을 계속 위로 보내야 하고 state 를 갖고 있는 최상위 부모가 해당 state를 수정하게 되면 바뀐 정보는 props로 모든 자식에게 내려가게 된다.