본문 바로가기

Today Learning _

Part 12-9. 리액트 살펴보기(state 끌어올리기)

 

동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 때가 있습니다.
이럴 때 가장 가까운 공통 상위의 state를 끌어올리는 것이 좋습니다.

 

 

주어진 온도에서 물의 끓는 여부를 추정하는 온도 계산기 만들기

BoilingVerdict라는 이름의 컴포넌트를 만듭니다. 
이 컴포넌트는 섭씨 온도를 의미하는 celsius prop을 받아서 온도고 물이 끓기에 충분한지 여부를 출력합니다.

 

BoilingVerdict component를 생성했습니다 :

 

해당 컴포넌트는 if문을 통해 celsius가 100도 이상일 때 return 값으로 물이 끓는다는 것을 나타냅니다.
아닐 경우에는 물이 끓지 않고 있다는 문구를 출력합니다.

 

 

그다음으로 Calculator라는 컴포넌트를 만듭니다.
이 컴포넌트는 온도를 입력할 수 있는 <input>을 렌더링하고 그 값을 this.state.temperature 에 저장합니다.

 

 

Calculator component를 생성했습니다 :

 

해당 컴포넌트는 constructor을 생성하고 그에 대한 handleChanage 이벤트를 생성합니다.
이후 결과 값에서 value를 this.state.temperature로 설정하고
변화값을 handleChange로 설정합니다.
또한 현재 입력값에 대한 BoilingVerdict 컴포넌트를 렌더링 합니다.

 

해당 컴포넌트에 100을 넣었을 때 결과 값입니다 :

 

 

 

 

 

 

두 번째 input 추가하기

새 요구사항으로 섭씨 입력 필드뿐만 아니라 1. 화씨 입력 필드를 추가하고

2. 두 필드 간에동기화 상태를 유지하도록 합니다.

 

Calculator에서 TemperatureInput 컴포넌트를 빼내는 작업부터 합니다.
또한 "c" 또는 "f"의 값을 가질 수 있는 scale prop을 추가할 것입니다.

 

 

변수로 scaleName을 지정해줍니다.

 

TemperatureInput component를 생성합니다 :

 

 

이제 Calculator 가 분리된 두 개의 온도 입력 필드를 렌더링하도록 변경할 수 있습니다.

 

 

Calculator 의 render 값을 TemperatureInput으로 나타냅니다 :

 

 

이제 두개의 입력 필드를 갖게 되었습니다.
그러나 둘 중 하나의 온도가 입력되더라도 다른 것은 업데이트되지 않는 문제가 있습니다.
이것은 두 입력 필드 간에 동기화 상태를 유지하고자 했던 목적과는 맞지 않습니다.

또한 Calculator에서 VoilingVerdict도 보여줄 수 없는 상황입니다.
현재 입력된 정보가 TemperatureInput 안에 숨겨져 있으므로 Calculator은 그 값을 알 수 없기 때문입니다. 

 

 

 

 

 

 

 

변환 함수 작성하기

 먼저 섭씨를 화씨로, 또는 그 반대로 변환해주는 함수를 작성합니다.

 

섭씨와 화씨로 나타낼 수 있는 함수를 각각 만듭니다 :

 

toCelsius는 화씨를 섭씨로 나타낼 수 있는 식을 적은 함수입니다.
반대로 toFahrenheit은 섭씨의 값을 화씨로 나타낼 수 있습니다.

 

이 두 함수는 숫자를 변환합니다.
temperature 문자열과 변환 함수를 인수로 취해서 문자열을 반환하는 또 다른 함수를 작성합니다.
그것을 한 입력값에 기반에 나머지 입력 값을 계산하는 용도로 사용합니다.

 

 

 

이 함수는 올바르지 않은 temperature 값에 대해서는 빈 문자열을 반환하고
값을 소수점 세 번째 자리로 반올림해 출력합니다.

 

 

 ex) tryConvert('abc', toCelsius)는 빈 문자열을 반환하고
       tryConvert('10.22', toFarenheit)는 '50.396'을 반환합니다.

 

 

 

 

 

 

State 끌어올리기

현재 두 TemperatureInput 컴포넌트가 각각의 입력값을 각자의 state에 독립적으로 저장하고 있습니다.
그러나 두 입력값이 서로의 것과 동기화된 상태가 되어야 합니다.
섭씨온도 입력값을 변경할 경우 화씨온도 입력값 역시 변환된 온도를 반영할 수 있어야 합니다.

 

TemperatureInput 컴포넌트 입니다 :

 

해당 컴포넌트를 보면 state를 가지고 있고 그 state가 컴포넌트 내에서
각각 처리된다는 것을 알 수 있습니다. 

 

리액트에서 state를 공유하는 일은 그 값을 필요로 하는 컴포넌트 간의 가장 가까운 조상으로부터 

state를 끌어올림으로써 이뤄낼 수 있습니다. 이런 방법을 'state 끌어올리기'라고 부릅니다.
TemperatureInput이 개별적으로 가지고 있던 state를 지우는 대신에 Calculator로 그 값을 옮겨놓을 것입니다.


Calculator가 공유될 state를 소유하고 있으면 이 컴포넌트는 두 입력 필드의
현재 온도에 대한 source of truth(진리의 원천)이 됩니다. 이를 통해 두 입력 필드가 서로 간에
일관된 값을 유지하도록 만들 수 있습니다. 두 TemperatureInput 컴포넌트의 props가 같은 부모
Calculator로부터 전달되기 때문에, 두 입력 필드는 항상 동기화된 상태를 유지할 수 있습니다.

 

 

우선 TemperatureInput 컴포넌트에서 this.state.temperature을 this.props.temperature로 대체합니다. 

this.props.temperature가 이미 존재한다고 가정할 때, 이 값은 Calculator부터 온 것입니다.

 

 

props는 읽기 전용입니다. temperature가 지역 state로 작동할 땐
그 값을 변경하기 위해 this.setState()를 호출하는 것으로 끝이었습니다.
그러나 temperature가 이제 prop으로 전달되기에 TemperatureInput은 그 값을 제어할 능력이 없습니다.

리액트에서는 보통 이 문제를 컴포넌트를 '제어'가능하게 만드는 방식으로 해결합니다.
DOM input이 value와 onChange prop를 건네받는 것과 비슷한 방식으로
사용자 정의된 TemperatureInput 역시 temperature와 onTemperatureChange props를 

자신의 부모인 Calculator로 건네받을 수 있습니다.

TemperatureInput에서 온도를 갱신하고 싶으면 this.props.onTemperatureChange를 호출하면 됩니다.

 

 

this.setState({temperature:e.target.value})를 변경한 값입니다.

 

** 사용자 정의 컴포넌트에서 temperature와 onTemperatureChange prop의 이름이 특별한 의미를 가지진 않습니다. 

일관된 컨벤션으로 value와 onChange을 사용할 수 있으며  개발자가 임의로 이름을 정할 수 있습니다.

 

 

onTemperatureChage prop는 부모 컴포넌트인 Calculator로부터 temperature prop와 함께 제공됩니다.
이를 이용해 자신의 지역 state를 수정해 변경 사항을 처리하므로 변경된 새 값을 전달받은 두 입력 필드는 모두 리 렌더링 될 것입니다.

 

 

Calculator의 변경사항을 들여다보기 전에 TemperatureInput 컴포넌트에 대한 변경사항부터 요약하겠습니다.

1. 이 컴포넌트의 지역 state를 제거했으며 this.state.temperature 대신 
    this.props.temperature 읽어왔습니다.
2. state를 변경하고 싶을 경우 this.setState() 대신에Calculator로부터 건네받은
    this.props.onTemperatureChange()를 호출하도록 만들었습니다.

 

변경된 TemperatureInput 컴포넌트 입니다 :

 

 

Calculator 컴포넌트에서는 temperature와 scale의 현재 입력 값을 이 컴포넌트의 지역 state에 저장합니다.
이것이 우리가 입력 필드들로부터 끌어올린 state이며 그들에 대한 source of truth로 작용할 것입니다.
또한 두 입력 필드를 렌더링 하기 위해 알아야 하는 모든 데이터를 최소한으로 표현한 것이기도 합니다.

 

예를 들어서 섭씨 입력 필드에 37을 입력하면 Calculator 컴포넌트의 state는 다음과 같을 것입니다.

{
    temperature:'37',
    scale:'c'
}

이후 화씨 입력 필드 값을 212로 수정하면 Calculator의 state는 다음과 같을 것입니다
{
    temperature:'212',
    scale:'f'
}

 

 

두 입력 필드에 모두 값을 저장하는 일도 가능했지만 결국은 불필요한 작업입니다.
가장 최근에 변경된 입력 값과 그 값이 나타나는 단위를 저장하는 것만으로 충분합니다.
이후 현재의 temperature와 scale에 기반해 다른 입력 필드의 값을 추론할 수 있습니다.

두 입력 필드의 값이 동일한 state로부터 계산되기에 이 둘은 항상 동기화된 상태를 유지하게 됩니다.

 

 

 

 

 

이제 어떤 입력 필드에 값을 적든 Calculatorthis.state.temperature와 
this.state.scale이 바뀝니다. 입력 필드 중 하나는 있는 그대로의 값을 받으므로
사용자가 입력한 값이 보존되고, 다른 입력 필드의 값은 다른 하나에 기반해 재계산됩니다.

 

 

동기화 된 값의 결과 1 :
동기화 된 값의 결과 2 :

 

 

입력 값을 변경할 때 일어나는 일들

1. 리액트는 DOM input의 onChange에 지정된 함수를 호출합니다.
   위의 예시는 TemperatureInput의 handleChange 메서드에 해당됩니다.

2. TemperatureInput 컴포넌트의 handleChange 메서드는 새로 입력된 값과 함께
    this.props.onTemperatureChange()를 호출합니다. onTemperatureChange를 포함한 컴포넌트의
    props는 부모 컴포넌트인 Calculator로부터 제공받은 것입니다.

3. 이전 렌더링 단계에서 Calculator는 섭씨 TemperatureInput의 onTemperatureChange를 
    Calculator의 handleCelsiusChange 메서드로 화씨 TemperatureInput의 onTemperatureChange
    Calculator의 handleFahrenheitChange 메서드로 지정해 놓았습니다. 따라서 우리가 둘 중 어떤 입력 필드를
    수정하느냐에 따라 두 메서드 중 하나가 호출됩니다.


4. 이 메서드는 내부적으로 Calculator 컴포넌트가 새 입력값, 그리고 현재 수정한 입력 필드의 입력 단위와 함께
    this.setState()를 호출하게 함으로써 React에게 자신을 다시 렌더링 하도록 요청합니다.


5. 리액트는 UI가 어떻게 보여야 할지 알아내기 위해 Calculator 컴포넌트의 render 메서드를 호출합니다.
    두 입력 필드의 값은 현재 온도와 활성화된 단위를 기반으로 재계산됩니다. 온도의 변환이 이 단계에서 수행됩니다.


6.  리액트는 Calculator가 전달한 새  props와 함께 각 TemperatureInput 컴포넌트의 render 메서드를 호출합니다.
    이후 UI가 어떻게 보여야 할지를 파악합니다.

7. BoilingVerdict 컴포넌트에게 섭씨온도를 props로 건네면서 그 컴포넌트의 render 메서드를 호출합니다.

8. 리액트 DOM은 물의 끓는 여부와 올바른 입력값을 일치시키는 작업과 함께 DOM을 갱신합니다.
   값을 변경한 입력 필드는 현재 값을 그대로 받고, 다른 입력 필드는 변환된 온도 값으로 갱신됩니다.

 

 

입력 필드 값을 변경할 때마다 동일한 절차를 거치고 두 입력 필드는 동기화된 상태로 유지됩니다.

 

 

모든 코드 보기 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

리액트 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 '진리의 원천'을 하나만 두어야 합니다.
보통 state는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가됩니다.
이후 다른 컴포넌트 역시 그 값을 필요로 하게 되면 그 값을 그들의 가장 가까운 공통 조상으로 끌어올리면 됩니다.

 

다른 컴포넌트 간에 존재하는 state를 동기화시키려고 노력하는 대신 하향식 데이터 흐름을 사용하는 것을 추천합니다.

 

**하향식 데이터 흐름 

 

Part 12-4. 리액트 살펴보기(State 와 Lifecycle)

State는 props와 유사해 보이지만 State는 컴퓨터 내부에서 사용하는 자료이므로 비공개 자료이다. 그리고 State는 데이터를 수정할 수 있는 공간이라고 할 수 있다. 즉, props가 component에 정보를 전달한 후 DO..

lsy0581.tistory.com

 

state를 끌어올리는 작업은 양방향 바인딩 접근 방식보다 더 많은 '보일러 플레이트 코드'를 유발하지만
버그를 찾고 격리하기 더 쉽게 만든다는 장점이 있습니다.

 

어떤 state든 간에 특정 컴포넌트 안에서 존재하기 마련이고 그 컴포넌트가 자신의 state를 스스로 변경할 수 있으므로
버그가 존재할 범위가 크게 줄어드는 것입니다. 또한 사용자의 입력을 거부하거나 변형하는 자체 로직 구현도 가능합니다.

 

* 보일러 플레이트 = 변경 없이 계속하여 재사용할 수 있는 재사용 가능한 프로그램을 의미

 

어떤 값이 props 또는 state로부터 계산될 수 있다면 그 값을 state에 두어서 안됩니다.
예를 들어 celsiusValue와 fahrenheitValue 둘 다 저장하는 대신
최근에 변경된 temperature와 scale만 저장하면 됩니다.

다른 입력 필드의 값은 항상 그 값에 기반에 계산되기 때문입니다.

이를 통해 사용자의 입력값의 정밀도도를 유지하고 다른 필드의 입력값에 반올림을 지우거나 적용할 수 있습니다.

 

또한 UI에서 무언가 잘못된 부분이 있을 경우 React Developer Tools를 이용해
props를 검사하고 state를 갱신할 책임이 있는 컴포넌트를 찾을 때까지  트리를 따라 탐색해 볼 수 있습니다.

 

 

 

 

 

 

 

참고 자료

 

State 끌어올리기 – React

A JavaScript library for building user interfaces

ko.reactjs.org