도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

함수형 setState가 리액트(React)의 미래이다(Functional setState is the future of React)

리액트(React)는 자바스크립트에서 함수형 프로그래밍(functional programming)을 대중화 했다. 이것이 리액트에서 사용하는 컴포넌트 기반 UI 패턴(the Component-based UI pattern)이 적용된 거대한 프레임워크로 이어져왔다. 이제는 함수형 웹 개발 방법이 생태계 전반에 확산되고 있다.


그러나 리액트 팀은 여기서 멈추지 않고, 훨씬 더 함수형 보석(functional gems)을 발견하면서 더 깊은 발굴을 계속하고 있다.

그래서 오늘 나는 당신에게 새로운 함수형 금(functional gold)을 밝히도록 하겠다. 리액트의 일급 비밀 - 함수형 setState (functional setState)!

좋아, 내가 이 이름은 내가 방금 지어낸 것이다. 이건 완전히 새로운 것도 비밀도 아니다. 아니, 정확하게 말하자면, 이것은 리액트에 이미 있는 패턴이다. 깊이 파고 들었던 소수의 개발자들만이 알고 있는.. 하지만 이것을 부르는 특정한 이름을 가지고 있지는 않았다. 하지만 이제는 - 함수형 setState!라고 한다.

이 패턴을 설명 할 때 Dan Abramov의 말대로, Functional setState는 이러한 패턴이다:

"컴포넌트 클래스와 별도로 상태(state) 변경을 선언하세요."

응?

당신이 이미 알고 있는 것

리액트는 컴포넌트 기반 UI 라이브러리이다. 컴포넌트는 기본적으로 일부 프로퍼티(property)를 허용하고 UI 엘리먼트를 반환하는 함수이다.

function User(props) {
return (
<div>A pretty user</div>
);
}

컴포넌트에는 상태(state)가 필요 할 수 있다. 이 경우 일반적으로 컴포넌트를 클래스로 작성한다. 그런 다음 클래스 생성자(constructor) 함수에 상태(state)를 선언한다:

class User {
constructor () {
this.state = {
score : 0
};

}
render () {
return (
<div>This user scored {this.state.score}</div>
);
}
}

state를 관리하기 위해 리액트는 setState()라는 스페셜 메소드를 제공한다. 당신은 이것을 다음과 같이 사용 할 수 있다:

class User {
...
increaseScore () {
this.setState({score : this.state.score + 1});
}
...
}

setState() 메소드의 동작 방식에 주목하자. 당신은 업데이트 할 상태(state)의 부분을 포함하는 객체를 setState 인자로 전달한다. 다시 말하면, 전달한 객체에는 컴포넌트 상태(state)의 키에 해당하는 키가 있고 setState()는 그 객체를 상태(state)에 머지(merge)하여 상태(state)를 업데이트하거나 새로 설정한다. 말그대로, "상태를 셋 한다(set-state)".

당신이 몰랐던 것

위에서 setState() 동작 방식에 대해 말한 것을 기억 하는가? 객체 대신 함수를 전달할 수 있다면 어떨까?

그렇다. setState()는 함수 역시 받아들인다. 함수는 다음(next) state를 계산하고 그 값을 리턴하기 위해 컴포넌트의 이전 state와 현재 props을 받는다. 아래에서 확인하자:

this.setState(function (state, props) {
return {
score: state.score - 1
}
}
);

setState()는 하나의 함수이며 다른 함수 (함수형 프로그래밍 ... 함수형 setState)를 전달하고 있다. 언뜻보기에, 이것은 이상해 보일 수도 있고, 상태를 세팅(set-state)하는데 지나치게 많은 단계가 포함되어 보일 수도 있다. 왜 이것이 필요할까?

setState에 함수를 전달할까?

문제는 state 업데이트가 비동기로 실행된다는데 있다.

setState()가 호출 될 때 어떤 일이 일어나는지 생각해보자. 리액트는 먼저 setState()에 전달한 객체를 현재 상태로 합친다. 그러면 reconciliation을 시작한다. 새로운 리액트 엘리먼트 트리 (UI의 객체 표현)를 만들고, 새 트리를 이전(old) 트리와 비교하고, setState()에 전달한 객체를 기반으로 변경된 부분을 파악한 다음 DOM을 최종적으로 업데이트한다.

휴! 사실, 이것 역시 엄청 요약한 것이다. 그러나 리액트를 믿자!

리액트는 단순하게 "상태를 세팅(set-state)"하지 않는다.

리액트는 여러 setState() 호출을 성능 향상을 위해 단일 배치 방식으로 작업을 할 수도 있다.

이게 무슨 뜻일까?

첫째, "여러 setState() 호출"은 setState()를 한 번 이상 단일 함수 내에서 호출하는 것을 의미 할 수 있다:

...
state = {score : 0};
// multiple setState() calls
increaseScoreBy3 () {
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});
this.setState({score : this.state.score + 1});

}
...

이제 리액트가 "상태 세팅(set-state)"을 세 번이나 하는 대신 "여러 setState() 호출"을 만났을 때 리액트가 위에서 설명한 많은 양의 작업을 피하고 현명하게 이렇게 말하는 것과 같다: "아니! 나는 이 산을 세 번 오르지 않고 매 상태의 일부를 옮겨서 업데이트 할 거야. 아니, 차라리 컨테이너를 가져다가 모든 조각을 모아서 한 번만 업데이트 하겠어." 배치로 처리 중이라는 것이다!

setState()에 전달하는 것은 일반 객체라는 것을 기억하자. 이제는 리액트가 "여러 setState() 호출"을 만나면 각 setState() 호출에 전달 된 모든 객체를 추출하여 배치 작업을 수행하고 이를 머지(merge)하여 단일 객체를 형성 한 다음 해당 단일 객체를 사용하여 setState()를 수행한다.

자바스크립트의 객체를 머지(merging)하는 것은 다음과 같을 수 있다 :

const singleObject = Object.assign(
{},
objectFromSetState1,
objectFromSetState2,
objectFromSetState3
);

이런 패턴을 오브젝트 컴포지션(object composition)이라고한다.

자바스크립트에서 만약 3개의 객체가 동일한 키를 갖고 있다면, Object.assign()에 마지막으로 전달 된 객체의 키 값이 우선된다. 예제:

const me  = {name : "Justice"}, 
you = {name : "Your name"},
we = Object.assign({}, me, you);
we.name === "Your name"; //true
console.log(we); // {name : "Your name"}

you가 we에 머지된(merged) 마지막 객체이기 때문에, you의 객체에 있는 name의 값 - "Your name"-이 me 객체에 있는 name의 값을 덮어 쓴다. 그래서 "Your name"이 we의 객체가 된다.

따라서, setState()에서 호출 할 때마다 매번 객체를 전달하고 리액트가 객체를 머지(merge)하게된다. 즉, 우리가 전달한 여러 객체 중에서 새로운 객체를 구성한다. 객체 중 하나에 동일한 키가 포함되어 있으면 동일한 키가 있는 마지막 객체의 키 값이 저장되는 것이다. 이해 되었는가?

즉, 위의 increaseScoreBy3 함수를 사용하면 리액트가 setState() 호출 순서로 상태를 즉시 업데이트하지 않기 때문에 함수의 최종 결과는 3 대신 1이 된다. 리액트는 이처럼 먼저 모든 객체를 하나로 결합한 후에 계산한다: {score : this.state.score + 1}, 새로 만든 객체로 "set-state"를 한 번만 수행했다. 이처럼 말이다: User.setState ({score : this.state.score +1}.

setState()에 객체를 전달하는 것이 문제가 아니다. 진짜 문제는 이전 상태에서 다음 상태를 계산할 때 객체를 setState()로 전달하는 것에 있다. 이것은 안전하지 않다!

this.propsthis.state가 비동기적으로 업데이트 될 수 있으므로 다음 상태(next state)를 계산할 때 해당 값을 신뢰해서는 안된다.

Sophia Shoemaker가 이 문제에 대해서 데모한 것이 있다. 이것을 확인해보고 이 데모에서 나쁜 솔루션과 좋은 솔루션에 주의를 기울여보자 :


함수형 setState가 답이다

위의 데모를 확인하지 않았다면 이 글의 핵심 개념을 파악하는 데 도움이되므로 꼭 체크 해보는 것이 좋다. 

위의 데모를 확인해보면, 함수형 setState가 우리의 문제를 해결 한 것을 알 수 있다. 하지만 정확히 어떻게 해결 한 것일까?

Dan의 트윗을 보자. 


그의 트윗을 유의해서 보자. 함수형  setState 쓸 때 ...

업데이트는 큐(queue)에 쌓이고 추후에 호출 된 순서대로 실행된다. 

따라서 리액트가 객체를 머지(merge)하는 대신 "여러 함수형 setState 호출"를 만나면 (물론 머지 할 객체가 없다) 리액트는 "호출 된 순서대로"함수를 큐에 넣는다. 

그 후 리액트는 "큐(queue)"의 각 함수를 호출하여 상태를 업데이트하고 이전 상태, 즉 첫 번째 함수형 setState() 호출 이전 상태를 전달한다 (첫 번째 함수형 setState() 인 경우).  만약 첫 번째 함수형 setState() 호출이 아니라면, 큐 내의 이전의 함수형 setState() 호출로부터 최신 갱신된 상태를 전달한다. 

다시 말하지만, 코드로 확인하는게 좋다고 생각한다. 이 예제 소스가 실제가 아니라는 것에 유의하자. 대신에 당신은 리액트에서 일어나는 일에 대한 아이디어를 얻을 수 있을 것이다. 

또한 덜 모호하게 하기 위해 ES6 문법을 사용한다. 언제든지 원한다면 나중에 ES5 문법을 쓸 수 있다.

먼저, 컴포넌트 클래스를 생성 해보자. 그런 다음 내부에 가짜 setState() 메소드를 만들 것이다. 또한, 우리 컴포넌트에는 여러 함수형 setState를 호출하는 increaseScoreBy3() 메서드가 있을 것이다. 마지막으로 리액트가 하는 것처럼 클래스를 인스턴스화한다. 

class User{
state = {score : 0};
//let's fake setState
setState(state, callback) {
this.state = Object.assign({}, this.state, state);
if (callback) callback();
}
// multiple functional setState call
increaseScoreBy3 () {
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) ),
this.setState( (state) => ({score : state.score + 1}) )
}
}
const Justice = new User();

setState는 두 번째 파라미터로 콜백 함수도 허용한다. 리액트가 상태를 업데이트 한 후 호출된다.

이제 사용자가 increaseScoreBy3()을 호출하면 리액트는 여러 함수형 setState를 큐에 담는다. 여기서는 페이크 로직을 만들지 않을 것이다. 우리는 함수형 setState를 실제로 안전하게 만드는 것에 중점을 두고 있기 때문이다. 그러나 당신은 그 "큐잉(queuing)" 프로세스의 결과를 다음과 같은 함수의 배열로 생각할 수 있다:

const updateQueue = [
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1}),
(state) => ({score : state.score + 1})
];

마지막으로, 업데이트 프로세스를 변경하자:

// recursively update state in the order
function updateState(component, updateQueue) {
if (updateQueue.length === 1) {
return component.setState(updateQueue[0](component.state));
}
return component.setState(
updateQueue[0](component.state),
() =>
updateState( component, updateQueue.slice(1))
);
}
updateState(Justice, updateQueue);

사실, 이것은 섹시한 코드가 아니다. 당신이 더 좋은 코드를 만들 수 있다고 생각한다. 그러나 여기서 핵심은 리액트가 함수형 setState에서 함수를 실행 할 때마다 리액트가 업데이트 된 상태의 새로운 복사본을 전달하여 상태를 업데이트한다는 것이다. 이것에 의해, 함수형 setState가 이전의 상태에 근거해서 상태를 셋(set)할 수 있는 것이다.

jsbin에서 실제 코드를 확인 할 수 있다.

완전히 이해하려면 위의 코드를 직접 수정 해보는 것도 좋다.

리액트 일급 비밀

지금까지 리액트에서 여러 함수형 setState를 수행하는 것이 왜 안전한지 깊게 살펴 봤다. 그러나 실제로 함수형 setState를 완벽하게 정의하지는 못했다: "상태 변경을 컴포넌트 클래스와 분리해서 작성하라."

수년간, setState에 전달하는 함수 또는 객체는 항상 컴포넌트 클래스 안에 존재했다.

새로 발견 된 보물, 즉 리액트의 일급 비밀을 선물하겠다:


Dan Abramov 덕분이다!

이것이 함수형 setState의 힘이다. 컴포넌트 클래스 외부에서 상태 업데이트 로직을 선언한다. 그런 다음 컴포넌트 클래스 내에서 호출한다.

// outside your component class
function increaseScore (state, props) {
return {score : state.score + 1}
}

class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}

이것은 선언적(declarative)이다! 컴포넌트 클래스는 더이상 상태 업데이트 방법을 고려하지 않는다. 단순히 원하는 업데이트 유형을 선언하기만 한다.

이 점에 깊이 감사하기 위해, 많은 상태(state)가 있는 복잡한 컴포넌트에 대해 생각해보자. 각 상태를 다른 액션으로 업데이트 할 것이다. 때로는, 각각의 업데이트 함수에는 많은 코드가 포함되어 있을 수 있다. 이 모든 로직이 컴포넌트 내에 있는 것이다. 그러나 더 이상은 그럴 필요가 없다!

또한, 나는 모든 모듈을 가능한 한 짧게 유지하는 것을 좋아하는데, 모듈이 너무 길어지는 느낌이 들 것이다. 지금부터는 모든 상태 변경 로직을 다른 모듈로 추출한 다음 컴포넌트에 가져 와서(import) 사용할 수 있다.

import {increaseScore} from "../stateChanges";
class User{
...
// inside your component class
handleIncreaseScore () {
this.setState( increaseScore)
}
...
}

이제는 다른 컴포넌트에서 increaseScore 함수를 다시 사용할 수도 있다.

함수형 setState로 다른 무엇을 할 수 있을까?

테스트 역시 쉽게 할 수 있다!


다음 상태를 계산하기 위해 추가 인자를 전달할 수도 있다.

더 많은 것을 기대하자...

리액트의 미래


수년 동안 리액트 팀은 stateful 함수를 가장 잘 구현하는 방법을 연구했다.

함수형 setState는 그 (아마)에 대한 답 인 것 같다.

Dan! 마지막으로?

여기까지 다 읽었다면, 당신은 아마도 나만큼 기쁠 것이다. 바로 이 함수형 setState를 테스트 해보자!

질문이 있거나 여기에있는 내용에 동의하지 않으면 여기 또는 Twitter를 통해 커멘트하기를 바란다.

해피 코딩!



이 글은 Justice Mba의 Functional setState is the future of React을 번역한 글입니다. 전문 번역가가 아니라 오역이 있을 수 있습니다. 지적해주시면 수정 하겠습니다. 원문은 아래에서 확인 할 수 있습니다. 감사합니다.


리뷰