[Pro react] 4장. 컴포넌트를 이용한 어플리케이션 구축

이번장에서는 컴포넌트가 중첩된 복잡한 사용자 인터페이스를 만드는 방법.

이를 위해 컴포넌트 API를 propTypes를 통해 노출하는 것의 중요성을 확인하고, 리액트 어플 내의 데이터 흐름에 대배 배우고, 컴포넌트를 조합하는 방법을 알아본다.

속성 유효성 검사

이것과 관련된 이야기는 여기를 참고해봅시다. 여기

컴포넌트 조합 전략과 모범 사례

이번 절에서는 컴포넌트를 조합해 리액트 애플리케이션을 만들기 위한 전략과 모범 사례를 다룬다. 좀 더 구체적으로 설명하자면 상태 관리와 데이터 가져오기(fetch), 사용자 상호작용을 더 체계적으로 제어하는 법을 알아본다.

상태 저장 컴포넌트와 순수 컴포넌트

컴포넌트는 상태(State)와 속성(Props)을 가짐을 알 수있다.

속성(Props)은 컴포넌트의 구성 정보에 해당한다. 속성은 상위 컴포넌트로부터 받으며, 이를 받은 컴포넌트 내에서는 변경할 수 없다.

상태(State)는 컴포넌트의 생성자에 정의된 기본값에서 시작해(일반적으로 사용자 이벤트에 의해)여러차례 변경될 수 있다. 컴포넌트는 자신의 상태를 내부적으로 관리한다. 또한 상태가 변경될 때마다 컴포넌트가 다시 렌더링된다.

리액트 컴포넌트에서 상태는 선택적이다. 대부분의 리액트 애플리케이션에서 컴포넌트는 상태를 관리하는 컴포넌트(상태 저장 컴포넌트)와 내부 상태가 없고 데이터를 표시하는 역할만 하는 컴포넌트(순수 컴포넌트)의 두가지 유형으로 나뉜다.

순수 컴포넌트의 목적은 속성을 받고 이를 뷰에 렌더링하는 것이 전부이므로 이러한 컴포넌트는 재사용하거나 테스트하기가 수월하다.

그런데 경우에 따라 사용자 입력, 서버 요청 또는 시간의 흐름에 반응해야 하는 상황이 있으며, 이를 위해 상태를 이용해야 한다. 상태 저장 컴포넌트는 일반적으로 컴포넌트 계층에서 상위를 차지하며, 상태 저장 컴포넌트나 순수 컴포넌트를 하나 이상 래핑한다.

앱의 컴포넌트는 대부분 상태 비저장으로 만드는 것이 좋다. 애플리케이션의 상태를 다수의 컴포넌트로 분산하면 관리하기 힘들고 애플리케이션의 작동 방식을 확인하기도 어렵기 때문이다. 또한 해결하기 아주 까다로운 문제가 생길 가능성이 있다.

어떤 컴포넌트가 상태 저장이어야 할까?

상태가 필요한 컴포넌트를 구분하는 것은 리액트 초보자에게는 가장 이해하기 어려운 문제 중 하나다. 어떤 컴포넌트가 상태를 가져야 하는지 확신이 서지 않을 때는 애플리케이션의 각 상태에 대해 다음의 4단계 검사 목록을 적용한다.

다음 예를 통해서 이해하자

데이터 흐름과 컴포넌트 통신

리액트 애플에서 데이터는 컴포넌트 계층에서 아래쪽으로 전달된다. 리액트는 프로그램이 어떻게 작동하는 쉽게 알수 있도록 이러한 흐름을 명시적으로 드러낸다.

그러나 아주 단순한 앱이 아니라면 중첩된 자식 컴포넌트가 부모 컴포넌트와 통신해야 하는 경우가 있다.

한 가지 방법은 부모 컴포넌트가 속성으로 전달한 콜백을 이용하는 것이다.

ContactApp 예제를 이용해 그 방법을 알아보자.

상태는 최상위 ContactApp 컴포넌트에 속하며 속성을 통해 SearchBar와 ContactList 컴포넌트로 전달된다.

그리고 사용자가 검색 폼을 수정할 때마다 사용자 입력을 반영해 상태를 업데이트해야 한다. 컴포넌트는 자신의 상태만 업데이트해야 하므로 ContactApp은 상태 업데이트가 필요할 때마다 호출할 콜백을 SearchBar로 전달한다. 입력에 대한 알림을 보내는 데는 onChange 이벤트를 이용할 수 있다. ContactApp에서 filterText상태를 변경하는 로컬 함수를 만들고 이 함수를 속성을 통해 SearchBar로 전달한다.

import React,{Component, PropTypes } from 'react'
import SearchBar from "./SearchBar";
import ContactList from "./ContactList";

// Renders a SearchBar and a ContactList
// Passes down filterText state and handleUserInput callback as props
class ContactsApp extends Component {
    constructor(){
        super();
        this.state={
            filterText: ''
        }
    }

    handleUserInput(searchTerm){
        this.setState({
            filterText:searchTerm
        })
    }

    render(){
        return(
            <div>
                <SearchBar filterText={this.state.filterText}
                           onUserInput={this.handleUserInput.bind(this)} />
                <ContactList contacts={this.props.contacts}
                             filterText={this.state.filterText}/>
            </div>
        )
    }
}
ContactsApp.propTypes = {
    contacts: PropTypes.arrayOf(PropTypes.object)
};

export default ContactsApp

는 속성을 통해 콜백을 받고 입력 필드의 onChange이벤트에 반응해 콜백을 호출한다. ```javascript import React, {PropTypes, Component}from 'react' // Pure component that receives 2 props from the parent // filterText (string) and onUserInput (callback function) class SearchBar extends Component { handleChange(event){ this.props.onUserInput(event.target.value) } render(){ return <input type="search" placeholder="search" value={this.props.filterText} onChange={this.handleChange.bind(this)} /> } } SearchBar.propTypes = { onUserInput: PropTypes.func, filterText: PropTypes.string.isRequired } export default SearchBar ``` 부모 컴포넌트에서 속성을 통해 filterText(문자열)와 onUserInput(콜백 함수)을 받는 순수 컴포넌트 의 경우 속성을 통해 contacts와 filterText를 받는 순수 컴포넌트이며 연락처를 필터링한 후 이를 표시하는 역할을 한다. 순수 컴포넌트라고 하는 이유는 동일한 contacts와 filterText속성을 전달하면 동일한 내용을 표시하기 때문이다. 렌더링에서 해결하는 것이 아니라 콜백함수라고 말할수 있는 렌더링 외부에서 함수를 선언해 변경이 있을 때마다 지켜본다. #### 컴포넌트 수명주기 리액트 컴포넌트를 만들 때 컴포넌트 수명주기의 특정 시점에 자동으로 호출될 메서드를 선언할 수 있다. 각 수명주기 메서드가 수행하는 역할과 이러한 메서드가 호출되는 순서를 이해하면 컴포넌트가 생성 또는 삭제될 때 특정한 작업을 수행할 수 있다. 또한 속성이나 상태 변경에 적절하게 대응하는 기회도 제공된다. 게다가 수명주기 메서드에 대한 전반적인 이해는 성능을 최적화하고 플럭스 아키텍처에서 컴포넌트를 구성하는 데도 필수다. ##### 수명주기 단계와 메서드 수명주기를 명확하게 이해하려면 초기 컴포넌트 생성 단계, 상태와 속성 변경, 트리거된 업데이트, 컴포넌트의 언마운트 단계 간의 차이를 구분할 수 있어야 한다. ![KakaoTalk_Photo_2017-06-28-22-09-58_34](http://i.imgur.com/26DwQRu.jpg) ![KakaoTalk_Photo_2017-06-28-22-09-59_63](http://i.imgur.com/cMOQaOG.jpg) ![KakaoTalk_Photo_2017-06-28-22-08-18_65](http://i.imgur.com/jUp52Fd.jpg) ![KakaoTalk_Photo_2017-06-28-22-08-20_34](http://i.imgur.com/9YfFOk8.jpg) #### 수명주기 함수의 실제 활용: 데이터 가져오기 수명주기 메서드의 실제 활용 예를 알아보자. 원격으로 가져오기! 데이터 가져오기는 리액트보다는 일반 자바스크립트에 대한 주제지만 컴포넌트의 특정 수명주기(componentDidMount 수명주기 메서드)에 데이터를 가져와야한다는 점이 중요하다. 원격 API와 통신하고 속성을 통해 데이터와 콜백을 전달하는 역할만 수행하는 상태 저장 컴포넌트를 새로 만드는 것이 권장되는 방법이다. 이러한 역할을 하는 컴포넌트를 컨데이너 컴포넌트라고 한다. 연락처 앱에서도 이 컨테이너 컴포넌트의 개념을 이용할 수 있다. 즉, 기존 ContactApp 컴포넌트에 데이터 가져오기 논리를 추가하는 것이 아니라 계층 위쪽에 ContactAppContainer라는 컴포넌트를 새로 만든다. 기존 ContactApp에는 변경사항이 없으며 여전히 속성을 통해 데이터를 받는다. 여기서 npm whatwg-fetch폴리필을 설치한다. 이는 통신을 위한 라이브러리로써 XMLHttpRequest보다 이용하기 쉽다. ```javascript componentDidMount(){ fetch('./contacts.json') .then((response) => response.json()) .then((responseData) => { this.setState({contacts: responseData}); }) .catch((error) => { console.log('Error fetching and parsing data', error); }); } ``` 일단 componentDidMount의 경우, 초기 렌더링이 다 끝난 직후 한 번 호출된다. 수명주기의 이 시점에서는 컴포넌트에 대한 DOM표현이 생성되며 이를 데이터 가져오기 등의 작업에 이용할 수 있다. #### 불변성에 대한 개요 리액트는 컴포넌트의 내부 상태를 변경하기 위한 setState 메서드를 제공한다. 컴포넌트의 UI상태를 업데이트할 때는 this.state를 직접 조작하는 것이 아니라 항상 setState 메서드를 이용해야 한다. this.state는 변경 불가로 받아들이는 것이 좋다. 여기에는 여러 이유가 있다. 우선 this.state를 직접 조작하는 것은 리액트의 상태 관리를 우회하는 것이다. 따라서 리액트 패러다임을 위반하게 되며, 나중에 setState()를 호출하면 이전에 수행한 변경이 무효화될 위험성까지 내포한다. 또한 this.state를 직접 조작하면 나중에 애플리케이션에서 성능을 개선할 수 있는 기회가 줄어든다. 성능 개선에 대해서는 이후 여러 장에서 다루겠지만 자바스크립트 객체가 변경됐는지 확인하는 검사와 객체 비교가 성능의 중요한 부분을 차지한다. 자바스크립트에서 객체 비교는 상당한 오버헤드를 유발하는 고비용 작업이지만 간단하고 빠른 방법이 있다. 객체가 변경된 경우 편집된 것이 아니라 대체된 것이므로 휠씬 빠르게 검사할 수 있다. (object1 === object2와 같이 객체 참조를 비교할 수 있다.) 이것이 객체를 변경하지 않고 대체하는 불변성의 기본 개념이다. #### 일반 자바스크립트에서의 불변성 불변성의 주 개념은 객체를 변경하지 않고 대체하는 것이며 알반 자바스크립트에서도 분명히 가능하지만 표준은 아니다. 주의하지 않으면 객체가 대체되지 않고 의도치 않게 변형될 수 있다. 예를 들어, 다음과 같이 항공사의 할인권에 대한 정보를 표시하는 상태 저장 컴포넌트가 있다고 가정해보자. (여기서는 컴포넌트의 상태만 검사하므로 render메서드는 생략했다.) ```javascript class Voucher extends Component { constructor(){ super(...arguments) this.state={ passengers:[ 'Simmon, Robert A.' 'Simmon, Robert R.' ], ticket:{ company: 'Dalta', flightNo: '0990', departure: { airport:'LAS', time:'2016-08-99' }, } } } } ``` 이렇게 있는 곳에서 이제 passengers 배열에 승객 한 명을 추가한다고 가정해보자. 이때 주의하지 않으면 의도치 않게 객체가 변형될 수 있다. 예를 들어 다음과 같다. ```javascript let updatedPassengers = this.state.passengers; updatedPassengers.push('Mitchell, Vincent M. ') this.setState({passengers: updatedPassengers}); ``` 짐작할 수 있겠지만, 이 예제 코드의 문제는 자바스크립트에서 객체와 배열은 참조로 전달된다는 점이다. 즉, updatedPassengers=this.state.passengers 같은 코드는 배열의 복사본은 만드는 것이 아니라 현재 컴포넌트의 상태에 있는 동일한 배열의 새로운 참조를 만드는 것이다. 또한 배열 메서드 push를 이용하면 내부 상태를 직접 변경하게 된다. 자바스크립트에서 실제 배열의 복사본을 만들려면 원래 배열을 변경하는 대신 원하는 변경이 적용된 배열을 반환하는 비파괴 메서드를 이용해야 한다. 이러한 비파괴 배열 메서드의 예로 map,filter, concat이 있다. 즉, 배열에 새로운 탑승자를 추가하는 문제를 다음과 같이 배열의 concat 메서드를 이용해 해결할 수 있다. ```javascript // updatedPassengers는 concat에서 반환하는 새로운 배열이다. let updatedPassengers = this.state.passengers.concat('Mitchell, Vincent M.'); this.setState({passengers:updatedPassengers}) ``` 대안으로는 Obejct.assign 등을 이용해 변경이 적용된 객체를 새로 생성하는 방법이 있다. Object.assign은 지정한 객체의 모든 속성을 대상 객체로 병합하는 방식으로 작업한다. Object.assign(target, source_1, ..., source_n) 이 코드는 먼저 source_1의 모든 열거 가능 속성을 대상으로 복사한 후 source_2로 진행해 같은 작업을 한다. ```javascript //updatedTicket은 this.state.ticket의 원래 속성과 새로운 flightNo를 병합한 새로운 객체다. let updatedTicket = Object.assign({}, this.state.ticket, {flightNo:'1010'}); this.setState({ticket:updatedTicket}); ``` #### 중첩된 객체 배열의 비파괴 메서드와 Obejct.assign으로 대부분의 경우를 해결할 수 있지만, 상태에 중첩된 객체나 배열이 들어 있으면 문제 해결이 아주 까다로울 수 있다. 그 이유는 자바스크립트 언어가 객체와 배열을 참조로 전달하며 배열의 비파괴 메서드나 Object.assign이 깊은 복사본을 만들지 않기 때문이다. 즉, 새로 반환되는 객체의 중첩된 객체와 배열은 사실 이전 객체에 있던 동일한 중첩된 객체와 배열을 참조한다. 앞의 예제에 나온 티켓 객체를 이용해 실제로 어떤 일이 일어나는지 확인해 보자. 이 말뜻은 let new Ticket=Object.assign({},originalTicket, {flightNo:'5690'}) `여기서 newTicket.arrival.airport ="MCO" 라고 변경하면, newTicket과 originalTicket 둘다 변한다. 이유는 깊은 복사본에 대한 정보는 같은 참조값을 가지고 있기 때문이다. 그러므로 사용되는 것이 update !!!! ` 을 하게되면 두개의 객체가 생성되는데, originalTicket의 참조로서 전달하였으므로, flightNo를 제외한 모든 정보는 동일하다. 이렇게 중첩된 객체를 포함하는 컴포넌트 상태를 변경할 때도 영향을 미친다. 원래 객체의 깊은 복제본을 만드는 방법이 있지만 성능 면에서 부담이 크고 경우에 따라 아예 불가능할 수 있기 때문에 좋은 방법이 아니다. 다행히 간단한 해결 방법이 있다. 리액트 에드온에는 복잡하고 중첩된 모델의 업데이트 작업을 도와주는 불변성 도우미 유틸리티 함수가 있다. ##### 리액트 불변성 도우미 리액트의 애드온 패키지에는 update라는 불변성도우미가 있다. update 함수는 일반 자바스크립트 객체와 배열을 대상으로 작업하며, 이러한 객체를 변경 불가로 조작할 수 있게 해준다. 즉, 실제로 객체를 변경하는 대신 변경된 새로운 객체를 반환한다. 우선 설치방법 >npm install --save react-addons-update 그리고 자바스크립트 파일에는 이렇게 임포트 >import update from 'react-addons-update' update메서드는 두 매개변수를 갖는다. 첫 번째 매개변수는 업데이트하려는 객체나 배열을 지정한다. 두 번째 매개변수는 변경을 수행할 "위치"와 수행할 변경의 "유형"을 나타내는 객체를 지정한다. 다음과 같이 학생의 이름(name)과 성적(grades)을 포함하는 간단한 객체가 있다고 가정하자. `let student={name:'John Caster', grades:['A','B','C']}` 이 객체의 성적을 업데이트하고 복사본을 생성하는 update 구문은 다음과 같다. `let newStudent=update(student, {grades:{$set: ['A','A','C']}})` 중첩할 수 있는 단계에는 제한이 없다. 도착정보를 변경하고 객체를 새로 만드는 데 문제가 있었던 이전 할인권 티켓 객체의 예로 돌아가보자. 원래 객체는 다음과 같다. ##### 배열 인덱스 배열 인덱스를 이용해 변경할 위치를 찾을 수 있다. 예를 들어, 첫 번째 codeshare 객체(0번 인덱스에 있는 배열 요소)를 변경하려면 다음과 같이 할 수 있다. ```javascript let newTicket = update(originalTicket, { codeshare: { 0:{$set: {company:'AZ', flightNo:'7320'}} } }) ``` ##### 사용할 수 있는 명령 | 명령 | 설명 | |---|---| | $push | 배열의 push와 비슷하게 배열 끝부분에 요소를 하나 추가한다. let initalArray= [1,2,3]; let newArray = update(initalArray, {$push:[4]}); >>[1,2,3,4] | | $unshift | 배열의 unshift와 비슷하게 배열 앞부분에 요소를 하나 추가한다. 예: let initalArray= [1,2,3]; let newArray = update(initalArray, {$unshift: [0]}); >>[0,1,2,3] | | $splice | 배열의 splice와 비슷하게 요소 제거 및/ 또는 추가를 통해 배열의 내용을 변경한다. 구문상의 주요차이점은 배열을 처리하기 위한 splice매개변수를 포함하는 배열의 배열을 제공해야 한다는 점이다. 예: let initalArray = [1,2,'a'] let newArray = update(initalArray, {$splice: [[2,1,3,4]]}); >>[1,2,3,4] | | $set | 대상을 완전히 대체한다. | | $merger | 지정한 객체의 키를 대상과 병합한다. 예: let obj={a:5, b:3}; let newObj = update(obj, {$merge:{b:6, c:7}}) >> {a:5, b:6, c:7} | | $apply | 현제 값을 지정한 함수로 전달하고 새로 반환된 값으로 이를 업데이트한다. 예: let obj={a:5,b:3} let newobj = update(obj, {b: {$apply: (value) => value*2}}) >> {a:5,b:6} | #### 칸반 앱 : 약간의 복잡성 추가하기. whatwg-fetch를 활용해서 외부데이터 가져오기 ```javascript import React, {Component} from 'react'; import KanbanBoard from './KanbanBoard'; import 'whatwg-fetch'; //서버를 로컬에서 실행하는 경우 기본 URL은 localhost:3000이며 로컬 서버에는 권한 부여 헤더가 필요없다. const API_URL = 'http://kanbanapi.pro-react.com'; const API_HEADERS = { 'Content-Type': 'application/json', Authorization: 'any-string-you-like' }; class KanbanBoardContainer extends Component { constructor() { super(...arguments); this.state = { cards: [], } } // 렌더링이 끝난 직후 한번 호출 된다 componentDidMount() { fetch(API_URL + '/cards', {headers: API_HEADERS}) .then((response) => response.json()) .then((responseDate) => { this.setState({cards: responseDate}) }) .catch((err) => { console.log('Error fetching') }) } render() { return <KanbanBoard cards={this.state.cards}/> } } export default KanbanBoardContainer ``` ##### 테스크 콜백을 속성과 연결 다음은 태스크를 조작하는 세 함수(addTask, deleteTask, toggleTask)를 작성해 보자. 테스크는 카드와 직접 연관되므로 모든 함수는 매개변수로 cardId를 받아야 한다. addTask는 새로운 태스크 텍스트를 받으면 deleteTask와 toggleTask는 taskId와 tastIndex(카드의 태스크 배열 내 위치)를 받아야 한다. 이러한 세 함수는 컴포넌트의 계층 아래쪽으로 속성을 통해 전달해야 한다. 새로운 함수를 전달할 때마다 속성을 생성하는 대신 입력할 코드의 양을 약간 줄이는 방법으로 세 함수를 참조하는 단일 객체를 생성하고 이를 단일 속성으로서 전달하는 방법이 있다. ```javascript import React, {Component} from 'react'; import KanbanBoard from './KanbanBoard'; import 'whatwg-fetch'; //서버를 로컬에서 실행하는 경우 기본 URL은 localhost:3000이며 로컬 서버에는 권한 부여 헤더가 필요없다. const API_URL = 'http://kanbanapi.pro-react.com'; const API_HEADERS = { 'Content-Type': 'application/json', Authorization: 'any-string-you-like' }; class KanbanBoardContainer extends Component { constructor() { super(...arguments); this.state = { cards: [], } } addTask(cardId, taskName){ } deleteTask(cardId, taskId, taskIndex){ } toggleTask(cardId, taskId, taskIndex){ } // 렌더링이 끝난 직후 한번 호출 된다 componentDidMount() { fetch(API_URL + '/cards', {headers: API_HEADERS}) .then((response) => response.json()) .then((responseDate) => { this.setState({cards: responseDate}) }) .catch((err) => { console.log('Error fetching') }) } render() { return <KanbanBoard cards={this.state.cards} taskCallbacks= /> } } export default KanbanBoardContainer ``` 이와 같이 작성하고 이제 약간 반복적인 작업이 남았다. 계층의 맨 위부터 CheckList 컴포넌트 사이에 있는 모든 컴포넌트(즉, KanbanBoard, List, Card 컴포넌트)는 부모로부터 taskCallbacks 속성을 받고 다시 이를 자식으로 전달해야 한다. 반복적인 작업처럼 보이기는 하지만 이 과정을 통해 컴포넌트 간 통신이 수행되는 방법을 명확하게 알 수 있다. ```javascript import React, {Component} from 'react'; import KanbanBoard from './KanbanBoard'; import 'whatwg-fetch'; //서버를 로컬에서 실행하는 경우 기본 URL은 localhost:3000이며 로컬 서버에는 권한 부여 헤더가 필요없다. const API_URL = 'http://kanbanapi.pro-react.com'; const API_HEADERS = { 'Content-Type': 'application/json', Authorization: 'any-string-you-like' }; class KanbanBoardContainer extends Component { constructor() { super(...arguments); this.state = { cards: [], } } addTask(cardId, taskName){ } deleteTask(cardId, taskId, taskIndex){ } toggleTask(cardId, taskId, taskIndex){ } // 렌더링이 끝난 직후 한번 호출 된다 componentDidMount() { fetch(API_URL + '/cards', {headers: API_HEADERS}) .then((response) => response.json()) .then((responseDate) => { this.setState({cards: responseDate}) }) .catch((err) => { console.log('Error fetching') }) } render() { return <KanbanBoard cards={this.state.cards} taskCallbacks= /> } } export default KanbanBoardContainer ``` 맨 상위 컴포넌트에서 어떤 콜백 함수를 활용할지 정의한 후, 속성으로 ```javascript taskCallbacks= ``` 와 같이 넘겨 준다! 넘겨주고 넘겨주고 마지막으로 체크리스트가서 받은 콜백함수들을 활용한다. ```javascript import React, { Component, PropTypes } from 'react'; class CheckList extends Component { checkInputKeyPress(e){ if(e.key === 'Enter'){ this.props.taskCallbacks.add(this.props.cardId, e.target.value); e.target.value = ''; } } render() { let tasks = this.props.tasks.map((task, taskIndex) => ( <li key={task.id} className="checklist__task"> <input type="checkbox" defaultChecked={task.done} onChange={this.props.taskCallbacks.toggle.bind(null, this.props.cardId, task.id, taskIndex)}/> {task.name}{' '} <a href="#" className="checklist__task--remove" onClick={this.props.taskCallbacks.delete.bind(null, this.props.cardId, task.id, taskIndex) }/> </li> )); return (
    {tasks}
<input type="text" className="checklist--add-task" placeholder="Type then hit Enter to add a task" onKeyPress={this.checkInputKeyPress.bind(this)}/>
); } } CheckList.PropTypes = { cardId: PropTypes.number, tasks: PropTypes.arrayOf(PropTypes.object), taskCallbacks: PropTypes.object }; export default CheckList; ``` ##### 태스크 조작 이 마지막 부분에서는 KanbanBoardContainer에서 태스크를 조작하며, 모든 변경 사항을 API를 이용해 서버에 저장하는 과정을 다룬다. 세 메서드(deleteTask, toggleTask, addTask)에서 현재 상태를 직접 조작하지 않도록 리액트의 불변성 도우미를 이용해야 한다. 설치는 > npm install --save react-addons-update 명령어 사용 그런데 작은 문제 하나가 있다. 불변성 도우미를 이용하려면 인덱스가 필요한데 KanbanList 컴포넌트에서 이미 카드를 필터링했기 때문에 원래의 인덱스에 접근할 수 없다. 따라서 각 요소에 대해 테스트 함수를 실행하고 이를 충족하는 요소의 인덱스를 반환하는 새로운 findIndex() 배열 메서드를 이용했다. 이건 kanban3을 참조해 주길 바랍니다! ##### 기본적인 낙관적 업데이트 롤백 예제를 보면 낙관적으로 UI를 변경했다는 것을 알 수 있다. 즉, 변경이 실제로 저장됐는지 여부에 대한 서버의 응답을 기다리지 않고 UI를 변경한다. 낙관적인 작업 방식은 체감 성능을 높이는 데 효과적이다. 사용자는 온라인 앱을 이용하면서 작업이 완료되기를 기다리고 싶어하지 않는다. 태스크를 원격 데이터베이스에 저장해야 한다는 것에 신경 쓰지 않으며 즉시 결과가 나오기를 원한다. 그런데 서버에서 오류가 발생하면 어떻게 해야 될까?? 몃 차례 재시도하고, UI변경을 되돌리며, 사용자에게 알리는 등의 과정이 피요하다. 낙관적 업데이트와 롤백은 쉬운 작업이 아니고 다양한 결과로 이어질 수 있지만, 이 예제에서는 변경불가 구조를 이용하는 작업 방식의 부수 효과 덕분에 기본적인 롤백 시나리오를 쉽게 구현할 수 있다. 즉, 이전 상태에 대한 참조를 유지하고 문제가 발생하면 원래대로 되돌릴 수 있다. 롤백 코드는 세 가지 콜백에서 동일하다. 먼저 컴포넌트의 원래 상태에 대한 참조를 저장한다. //낙관적인 UI변경을 되돌려야 하는 경우를 대비해 //변경하기 전 원래 상태에 대한 참조를 저장한다. let prevState = this.state; fetch명령이 실패하거나 서버 응답 상태가 정상이 아닌 경우 setState를 이용해 원래 상태로 되돌린다. ```javascript fetch(...,{...}) .then() .catch((err) => { this.setstate(prevState); }) ``` ## 정리 3장에서는 리액트에서 복잡한 UI를 구성하는 방법을 배웠다. 리액트 애플리케엿ㄴ에서 데이터는 항상 부모 컴포넌트에서 자식 컴포넌트로 한 방향으로만 전달된다는 것을 확인했다. 컴포넌트 간의 통신을 위해서는 부모 컴포넌트가 속성을 통해 콜백 함수를 자식 컴포넌트로 전달해 보고할 수 있게 한다. 또한 컴포넌트를 내부 상태를 조작하는 상태 저장 컴포넌트와 내부 상태가 없고 속성을 통해 받은 데이터를 표시하는 일만 하는 순수 컴포넌트의 두 분류로 구분하는 것이 컴포넌트 재사용에 바람직하다는 사실과 그 이유를 살펴봤다. 일반적으로 애플맄이션 컴포넌트 계층의 최상위 레벨에 배치되는 상태 저장 컴포넌트의 수를 가급적 적게 유지하고 순수 컴포넌트를 더 많이 만드는 것이 바람직하다. 마지막으로 컴포넌트의 상태를 변경 불가로 취급해 항상 this.setState를 이용해 변경해야 하는 이유와 리액트의 불변성 도우미를 이용해 변경된 this.state의 얕은 복사본을 생성하는 방법을 알아봤다!