본문 바로가기
Language/React

[React 공식 Doc 가이드 #11] Lifting State Up

by ocwokocw 2021. 2. 11.

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.

- Lifting State Up

때로는 같은 data에 대하여 여러개의 Component가 영향을 받는 경우가 있다. 이런 경우에 해당 Component 들의 최소 공통 조상 레벨로 state 를 올려서 공유하면 유용하다.

 

이번 Section 에서는 주어진 온도에서 물이 끓을 수 있느냐를 판단하는 온도계산기를 만들면서 State를 올린다는게 무슨말인지 이해해보자.

 

처음에 BoilingVerdict 라는 Component 로 시작을 할 것이다. 섭씨 온도를 prop 으로 받아서 물이 끓을 수 있는지 여부를 출력하는 기능을 가지고 있다.

 

function BoilingVerdict(props){
	if(props.celsius >= 100){
		return (<p> The water would boil.</p>);
	}
	return (<p>The water would not boil.</p>);
}


ReactDOM.render(
	<BoilingVerdict celsius={111} />, 
	document.getElementById('root')
);

 

celsius attribute 에 따라서 조건에 따른 p 태그를 출력한다. 

 

<input>에 온도를 입력하면 this.state.temperature에 value 를 가지고 있는 Calculator Component 만들어보자. 온도를 입력해서 물이 끓느냐를 출력하는 부분은 앞에서 만든 BoilingVerdict Component 를 재사용 할 것이다.

 

class Calculator extends React.Component{
	
	constructor(props) {
		super(props);
		this.state = {
			temperature: ''
		};
		
		this.handleInput = this.handleInput.bind(this);
	}
	
	handleInput(event) {
		this.setState({temperature: event.target.value});
	}
	
	render() {
		
		const temperature = this.state.temperature;
		
		return (
			<fieldset>
				<legend>Enter temperature in Celsius:</legend>
				<input name="temperature"
					onChange={this.handleInput} />
				<BoilingVerdict celsius={temperature} />
			</fieldset>
		);
		
	}
	
}


ReactDOM.render(
	<Calculator />, 
	document.getElementById('root')
);


- Adding a Second Input

새로운 요구사항을 추가해보자. 섭씨 input 에 이어서 화씨 input 을 추가하고 두 값을 동기화 시킨다.

 

앞에서 만든 Calculator Component 를 살펴보자. 출력부는 변하지 않겠지만 섭씨와 화씨를 입력해야 하므로, 입력부분을 새로운 Component 로 추출하고 이를 TemperatureInput 이라고 정의하자.

섭씨 화씨 구분을 위해 prop에 scale 변수로 "c", "f" 로 넘겨줄 것이다.

 

const scaleNames = {
	c: 'celsius',
	f: 'Fahrenheit'
};

class TemperatureInput extends React.Component{
	constructor(props){
		super(props);
		this.state = {
			temperature: ''
		};
		
		this.handleInput = this.handleInput.bind(this);
	}	
	
	handleInput(event) {
		this.setState({temperature: event.target.value});
	}
	
	render() {
		const temperature = this.state.temperature;
		const scale = this.props.scale;
		
		return (
			<fieldset>
				<legend>Enter temperature in {scaleNames[scale]}:</legend>
				<input name="temperature"
					value={temperature}
					onChange={this.handleInput} />
			</fieldset>
		);
	}
}

 

이제 Caculator 는 2개의 input 을 rendering 하게 되었다.

 

class Calculator extends React.Component{
	
	constructor(props) {
		super(props);
	}
	
	render() {
		
		return (
			<div>
				<TemperatureInput scale='c' />
				<TemperatureInput scale='f' />
			</div>
		);
		
	}
}

그런데 문제가 있다. 결과를 보면 알겠지만 섭씨를 바꿀때 화씨가 동기화되고 그 역도 되어야 하는데 그렇지 않다. 그리고 BolingVerdict component 로 출력을 하지 않는다. 온도값에 따라 출력을 해야 하지만 일단 온도값을 어떻게 참조 해야 할지 정하지 못해서 출력부분을 임시로 제거하였다.


- Writing Conversion Functions

우선 화씨 -> 섭씨, 그 반대를 계산하는 함수를 작성해야 한다.

 

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

 

위의 두 함수는 숫자로 변환한다. 따라서 1번째 인자인 온도를 string 형태도 받고, 2번째 인자로 화씨, 섭씨 변환함수를 받아서 string 으로 return 하는 함수를 작성해보자.

 

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

 

해당함수는 유효하지 않은 온도에 대해서 빈 문자열을 return 하고, 유효한 숫자값에 대해서는 소수점 아래 3자리 까지 되돌려줄 것이다. ex) tryConvert('abc', toCelsius) -> "", tryConvert('10.22', toFahrenheit) -> '50.396'


- Lifting State Up

현재 2개의 TemperatureInput 은 각자 state 를 따로 가지고 있지만 섭씨 화씨 온도를 동기화 해야 한다. React 에서 state 를 공유하는 법은 가장 가까운 공통 조상 레벨의 Component 로 공유할 값을 올리면 된다. 이를 "lifting state up" 이라고 부른다. 각자 가지고 있는 온도값을 제거하고 TemperatureInput 의 온도를 Calculator 로 옮겨보자.

 

Calculator 가 공유 state 를 소유하게 되면 두개의 input 에 대해 "source of truth"(controlled component 참조)의 개념이 된다. 그렇게 되면 2개의 Component 의 value 는 일관성을 가지게 된다. 두 개의 TemperatureInput Component 들은 부모의 Calculator component 로 부터 prop 을 통해 참조하기 때문에 2개의 input 은 언제나 동기화 된다.

어떻게 가능한지 천천히 살펴보자.

 

우선 Temperature component 안에서 this.state.temperature 부분을 this.props.temperature 로 변경한다. 나중에는 Calculator 에서 해당값을 넘겨주게 바꿔야 할 것이다.

 

render() {
		const temperature = this.props.temperature;
		const scale = this.props.scale;

 

알다시피 props 는 read-only 이다. temperature 를 따로따로 가지고 있었을 때는 TemperatureInput 에서는 단지 this.setState() 호출만 가능했었다. 그러나 지금은 temperature 가 부모로 부터 오는 prop 으로 변경되었기 때문에 TemperatureInput 은 해당값을 더이상 제어하지 않는다.

 

이러한 해결방식을 "controlled" component 라고 칭한다. 단지 DOM <input> 은 value 를 받아서 onChange 의 prop 으로 넘겨주면 TemperatureInput component 에서는 부모로 부터 temperature 와 onTemperatureChange 를 props 로 받아서 적절한 처리를 할 수 있다.

 

이제 TemperatureInput 은 temperature 를 갱신할 때 this.props.onTemperatureChange 를 호출할 것이다.

 

handleInput(e) {
		//this.setState({temperature: event.target.value});
		this.props.onTemperatureChange(e.target.value);
	}

 

onTemperatureChange prop 은 Calculator Component 로 부터 temperature 와 함께 prop 으로 전달된다. onTemperatureChange 함수는 온도값이 변경 될 때 그 변화를 handling 하고, 두 input 은 새로운 값으로 re-rendering 된다.

 

Caculator 를 변경하기 전에, TemperatureInput component 변화를 요약해보자. 

1. TemperatureInput component 안에 있던 state(temperature) 를 제거하고, props 로 해당값을 참조하게 변경하였다.

2. this.setState() 로 변경하던 부분을 Calculator component 로 부터 넘어온 함수(this.props.onTemperatureChange()) 를 호출하게 변경하였다.

 

class TemperatureInput extends React.Component{
	constructor(props){
		super(props);
		
		this.handleInput = this.handleInput.bind(this);
	}	
	
	handleInput(e) {
		this.props.onTemperatureChange(e.target.value);
	}
	
	render() {
		const temperature = this.props.temperature;
		const scale = this.props.scale;
		
		return (
			<fieldset>
				<legend>Enter temperature in {scaleNames[scale]}:</legend>
				
			</fieldset>
		);
	}
}

 

TemperatureInput 의 최종코드는 위와 같이 변경된다. 이제 Calculator component 를 변경해보자.

 

현재 temperature 와 scale 을 state 로 가지고 있다. 이 state 는 input 태그들로 부터 "lifted up" 되었고, 두 입력으로 부터 단일 소스 저장소 역할(source of truth)을 한다. 이 2개의 state 값들이 우리의 요구사항을 rendering 하기 위한 최소 조건이다.

 

만약 우리가 섭씨 37도를 입력한다면 Calculator 의 component 는 {temperature: '37', scale: 'c'} 가 될 것이고 화씨 212 를 입력한다면 {temperature: '212', scale: 'f'} 가 될 것이다.

 

가장 최근에 입력된 숫자값과 해당 숫자값이 섭씨 화씨를 나타내는지만 알면 된다. 같은 state 로 부터 값이 계산되기 때문에 input 들간의 동기화가 되는 것이다.

 

class Calculator extends React.Component{
	
	constructor(props) {
		super(props);
		this.state = {
			scale: 'c',
			temperature: ''
		};
		
		this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
		this.handleFahrenheightChange = this.handleFahrenheightChange.bind(this);
	}
	
	handleCelsiusChange(temperature) {
		this.setState({
			scale: 'c',
			temperature: temperature});
	}
	
	handleFahrenheightChange(temperature){
		this.setState({
			scale: 'f',
			temperature: temperature});
	}
	
	render() {
		
		const scale = this.state.scale;
		const temperature = this.state.temperature;
		const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
		const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
		
		return (
			<div>
				<TemperatureInput scale='c' 
					temperature={celsius}
					onTemperatureChange={this.handleCelsiusChange}/>
				<TemperatureInput scale='f' 
					temperature={fahrenheit}
					onTemperatureChange={this.handleFahrenheightChange}/>
			</div>
		);
		
	}
}

이제 어떤 input 박스에 입력을 하던간에 Calculator 의 this.stats.scale 과 this.state.temperature 값은 갱신 된다. 하나의 input 값을 입력하게 되면 다른 input 값은 해당값을 기반으로 언제나 다시 계산된다.

 

input 을 입력할 때 무슨일이 일어나는지 차례대로 살펴보자.

1. React 는 <input> 태그의 onChange 를 호출한다. 예제의 경우엔 TemperatureInput component 의 handleChange 메소드가 해당 메소드가 된다.

 

2. TemperatureInput의 handleChange 는 다시 this.props.onTemperatureChange() 를 입력된 값을 전달하면서 호출한다. onTemperatureChange 는 부모 Component 인 Calculator 로 부터 온 것이다.

 

3. 섭씨 TemperatureInput 의 onTemperatureChange 는 Calculator 의 handleCelsiusChange 메소드 이고, 화씨 TemperatureInput의 onTemperatureChange 는 Calculator 의 handleFahrenheightChange 메소드이다. 어떤 input 값이 변경되냐의 따라서 Calculator 의 위의 두 메소드중 하나의 메소드가 호출 된다.

 

4. 3.에서 호출된 메소드 안에서 Calculator component 는 this.setState() 를 호출해서 state 를 변경하고 React 는 re-rendering 을 하게 된다.

 

5. React 는 Calculator component 의 render 메소드를 호출해서 UI 를 변경한다. 입력한 scale(섭씨, 화씨) 여부와 현재 온도를 기반으로 두 input 의 값은 모두 다시 계산 된다. 이 때 온도 변환이 일어난다.

 

6. React 는 Calculator 로 부터 전달된 새로운 props 와 함께 각 TemperatureInput 의 render 메소드들을 호출한다. 이때 UI 의 어떤점이 변경되어야 하는지 알게 된다.

 

7. React 는 BoilingVerdict 의 render 메소드를 호출하고, 섭씨 온도를 props 로 전달한다.

 

8. React DOM 은 입력된 값과 비교하여 boiling verdict DOM 을 갱신한다. 우리가 섭씨를 입력했따면 섭시 input 은 현재값을 단순히 표시하고, 다른 input 은 변환되어 갱신된다.


- Lessons Learned

React app 에서 변경되는 data 는 하나의 소스 저장소("single source of truth") 가 되어야 한다. 대부분의 state 는 Component 에 rendering 을 위해서 처음에 초기화 된다. 그리고 만약 다른 Component 도 같은 값이 필요하게 되면, 필요한 두 개의 Component 의 최소 공통 조상 레벨로 "lift up" 하면 된다. 서로 다른 두개의 state 를 동기화 시키지 말고, top-down data 흐름 구조를 이용해야 한다.

 

이런 Lifting state 는 two-way binding 접근 방식보다 많은 코드를 필요로 하지만 bug 를 찾기 쉽게 해준다. 

댓글