리액트 기본 배우기 -state를 공유하는 방법-

1. shared state

 

말 그대로 공유된 state

 

자식 컴포넌트들이 가장 가까운 공통된 부모 컴포넌트의 state를 공유해서 사용한다

 

shared state는 어떤 컴포넌트의 state에 있는 데이터를 여러개의 하위 컴포넌트에서 공통적으로 사용하는 경우

 

 

가장 위에 있는 컴포넌트는 부모 컴포넌트이고, 아래 화살표로 연결된 2개의 컴포넌트는 자식 컴포넌트

 

부모 컴포넌트는 value라는 값을 가지고 있다.

 

왼쪽 아래에 있는 컴포넌트 A는 값에 2를 곱해서 표시하고, 오른쪽 아래의 B는 값에 3을 곱해서 표시한다.

 

이런 경우에 자식 컴포넌트들이 각각 값을 갖고 있을 필요는 없다.

 

그냥 부모 컴포넌트의 state의 값에 각각 2와 3을 곱해서 표시하면 그만이다

 

 

 

위 그림도 마찬가지로 3개의 컴포넌트가 있다

 

부모 컴포넌트의 degree라는 이름의 섭씨온도 값을 가지고 있으며 왼쪽 아래의 C는 온도를 섭씨로 표현해주고

 

오른쪽 아래의 F는 온도를 화씨로 표현해주는 컴포넌트이다.

 

자식 컴포넌트들이 각각 온도 값을 가지고 있을 필요 없이, 그냥 부모 컴포넌트의 state에 있는 섭씨온도 값을 변환해서 표시해주면 된다.

 

 

2. 하위 컴포넌트에서 state 공유하기

 

2-1) 먼저 섭씨온도 값을 props로 받아 물이 끓는지 안끓는지를 문자열로 출력해주는 컴포넌트 작성

 

function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>물이 끓습니다.</p>
    }
    return <p>물이 끓지 않습니다.</p>
}

 

위 코드는 섭씨온도값을 props로 받아서, 100'C이상이면 물이 끓는다는 문자열을 출력하고,

 

그 외에는 물이 끓지 않는다는 문자열을 출력한다

 

2-2) 이제 이 컴포넌트를 사용하는 부모 컴포넌트를 작성

 

function Calculator(props) {
    const [temperature, setTemperature] = useState('')

    const handleChange = (event) => {
        setTemperature(event.target.value)
    }

    return (
        <fieldset>
            <legend>섭씨 온도를 입력하세요:</legend>
            <input
            value = {temperature}
            onChange={handleChange} />
            <BoilingVerdict
            celsius={parseFloat(temperature)} />
        </fieldset>
    )
}

 

위 코드의 Calculator는 state로 tempeature 하나만 가지고 있다.

 

사용자로부터 입력을 받기 위해 <input> 태그에 제어 컴포넌트 형태로 구현

 

사용자가 온도 값을 변경할때마다 handleChange 함수가 호출되어 setTemperature 함수로 온도 값을 갖고 있는 temperature라는 이름의 state를 업데이트

 

그리고 state에 있는 온도값이 BoilingVerdict라는 컴포넌트에서 celsius라는 이름의 props로 전달

 

 

2-3) 이제 온도를 입력하는 부분을 별도의 컴포넌트로 추출

 

이러면 섭씨온도와 화씨온도를 각각 따로 입력받을 수 있도록 하여 재사용이 가능한 형태로 컴포넌트를 만들어 사용하는 것이 효율적이라

 

const scaleNames = {
    c:'섭씨',
    f:'화씨',
}

function TemperatureInput(props) {
    const [temperature, setTemperature] = useState('')

    const handleChange = (event) => {
        setTemperature(event.target.value)
    }

    return (
        <fieldset>
            <legend>온도를 입력해 주세요(단위:{scaleNames[props.scale]}):</legend>
            <input value={temperature} onChange={handleChange}/>
        </fieldset>
    )
}

 

위 코드는 온도를 입력받기 위한 temperatureinput 컴포넌트

 

calculator 컴포넌트에서 온도를 입력받는 부분을 추출하여 별도의 컴포넌트로 만든것이다.

 

추가적으로 props에 단위를 나타내는 scale을 추가해서 온도의 단위를 섭씨 또는 화씨로 입력 가능하도록 만들었다.

 

이렇게 추출한 컴포넌트를 사용하도록 calculator를 변경하면..

 

function Calculator(props) {
    return (
        <div>
            <TemperatureInput scale="c" />
            <TemperatureInput scale="f" />
        </div>
    )
}

 

총 2개의 입력을 받을 수 있도록 했고, 하나는 섭씨온도, 다른 하나는 화씨온도를 입력받는다.

 

그런데 여기서 문제는 TemperatureInput의 state에 사용자가 입력하는 값이 저장되므로,

 

섭씨온도와 화씨온도를 따로 입력받으면 2개의 값이 서로 다를 수 있다

 

 

2-4) 이를 위해 값을 동기화해야한다.

 

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

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

 

위 코드는 섭씨를 화씨로, 화씨를 섭씨로 변경해주는 함수

 

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()
}

 

tryConvert() 함수는 온도 값과 변환하는 함수를 파라미터로 받아서 값을 변환시켜 리턴해주는 함수

 

숫자가 아닌 값을 입력하면 empty string을 리턴하도록 예외 처리를 했다

 

tryConvert('abc', toCelsius) //''

tryConvert('10.22', toFahrenheit) //'50.396'

 

2-5) 이제 하위 컴포넌트의 state를 공통된 부모 컴포넌트로 공유해보자.

 

여기서 state를 상위 컴포넌트로 올린다는 것을 state 끌어올리기(lifting state up)라고 함

 

먼저 temperatureInput의 temperature를 입력받는 부분을 props.temperature로 수정

 

const scaleNames = {
    c:'섭씨',
    f:'화씨',
}

function TemperatureInput(props) {
    const [temperature, setTemperature] = useState('')

    const handleChange = (event) => {
        setTemperature(event.target.value)
    }

    return (
        <fieldset>
            <legend>온도를 입력해 주세요(단위:{scaleNames[props.scale]}):</legend>

            {/* 변경 전: <input value={temperature} onChange={handleChange} */}
            <input value={props.temperature} onChange={handleChange}/>
        </fieldset>
    )
}

 

이러면 state의 temperature를 가져오는게 아니라, props를 통해 가져오게 된다.

 

컴포넌트의 state를 사용하지 않으므로, 입력값이 변경되었을때, 상위 컴포넌트로 변경된 값을 전달해줘야함

 

이를 위해 handleChange 함수에서 setTemperature를, props.onTemperatureChange()로 변경시켜준다

 

const scaleNames = {
    c:'섭씨',
    f:'화씨',
}

function TemperatureInput(props) {
    const [temperature, setTemperature] = useState('')

    const handleChange = (event) => {

        // 변경 전: setTemperature(event.taget.value)
        props.onTemperatureChange(event.target.value)
    }

    return (
        <fieldset>
            <legend>온도를 입력해 주세요(단위:{scaleNames[props.scale]}):</legend>

            {/* 변경 전: <input value={temperature} onChange={handleChange} */}
            <input value={props.temperature} onChange={handleChange}/>
        </fieldset>
    )
}

 

이러면 온도 값을 변경할때마다 props에 있는 onTemperatureChange()로 변경된 온도 값이 상위 컴포넌트로 전달된다.

 

 

2-6) 이제 state는 필요없으니 제거하면 오직 상위 컴포넌트에서 전달받은 값만을 사용한다

 

const scaleNames = {
    c:'섭씨',
    f:'화씨',
}

function TemperatureInput(props) {

    const handleChange = (event) => {

        // 변경 전: setTemperature(event.taget.value)
        props.onTemperatureChange(event.target.value)
    }

    return (
        <fieldset>
            <legend>온도를 입력해 주세요(단위:{scaleNames[props.scale]}):</legend>

            {/* 변경 전: <input value={temperature} onChange={handleChange} */}
            <input value={props.temperature} onChange={handleChange}/>
        </fieldset>
    )
}

 

이렇게 변경된 temperatureInput 컴포넌트에 맞춰 calculator 컴포넌트도 변경해준다

 

function Calculator(props) {
    const [temperature, setTemperature] = useState('')
    const [scale,setScale] = useState('c');

    const handleCelsiusChange = (temperature) => {
        setTemperature(temperature)
        setScale('c')
    }

    const handleFahrenheitChange = (temperature) => {
        setTemperature(temperature)
        setScale('f')
    }

    const celsius = scale === 'f' ? tryConvert(temperature,toCelsius) : temperature;
    const fahrenheit = scale === 'c'? tryConvert(temperature, toFahrenheit) : temperature;

    return (
        <div>
            <TemperatureInput
            scale='c'
            temperature={celsius}
            onTemperatureChange={handleCelsiusChange}/>
            <TemperatureInput
            scale='f'
            temperature={fahrenheit}
            onTemperatureChange={handleFahrenheitChange}/>
            <BoilingVerdict
            celsius={parseFloat(celsius)}/>
        </div>
    )
}

 

 

state로 temperature와 scale을 선언하여 온도 값과 단위를 각각 저장하도록 한다

 

온도와 단위를 이용해 변환함수로 섭씨온도와 화씨온도를 구해서 사용

 

temperatureInput 컴포넌트를 사용하는 부분에 각 단위로 변환된 온도 값과 단위를 props로 넣어주고

 

값이 변경되었을때 업데이트를 위한 함수를 onTemperatureChange에 넣어준다

 

그래서 섭씨온도가 변경되면 단위가 'c'로 변경되고 화씨면 'f'로 변경된다

 

 

 

상위 컴포넌트인 calculator에서 온도 값과 단위를 각각의 state로 가지고 있고,

 

2개의 하위 컴포넌트는 각각 섭씨와 화씨로 변환된 온도 값과 단위, 그리고 온도를 업데이트하기 위한 함수를 props로 가지고 있다.

 

이렇게 각 컴포넌트가 state에 값을 가지고 있지 않고, 공통된 상위 컴포넌트로 올려서 공유하는 방법으로 리액트에서 더  간결하고 효율적인 개발이 가능하다.

 

 

3. 실습으로 따라해보기

 

$npx create-react-app my-app으로 프로젝트 생성

 

src폴더 내에 chapter_12라는 폴더 생성,

 

TemperatureInput.jsx라는 파일을 만들고 코드 작성

 

const scaleNames = {
    c:'섭씨',
    f:'화씨',
}

function TemperatureInput(props) {
    const handleChange = (event) => {
        props.onTemperatureChange(event.target.value)
    }

    return (
        <fieldset>
            <legend>
                온도를 입력해주세요(단위:{scaleNames[props.scale]})
            </legend>
            <input value={props.temperature} onChange={handleChange}/>
        </fieldset>
    )
}

export default TemperatureInput;

 

TemperatureInput 컴포넌트는 props로 scale과 temperature를 받아 표시해주며 온도가 변경되면

 

props의 onTemperatureChange함수를 호출해서 상위 컴포넌트로 변경된 값을 전달해준다

 

이제 동일 폴더에 Calculator.jsx파일을 만들고 코드를 작성

 

import React, {useState} from "react";
import TemperatureInput from './TemperatureInput';

function BoilingVerdict(props) {
    if (props.celsius >=100){
        return <p>물이 끓습니다.</p>
    }
    return <p>물이 끓지 않습니다.</p>
}

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

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

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()
}

function Calculator(props) {
    const [temperature, setTemperature] = useState('')
    const [scale, setScale] = useState('c')

    const handleCelsiusChange = (temperature) => {
        setTemperature(temperature)
        setScale('c')
    }

    const handleFahrenheitChange = (temperature) => {
        setTemperature(temperature)
        setScale('f')
    }

    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c'? tryConvert(temperature, toFahrenheit) : temperature;

    return (
        <div>
            <TemperatureInput
            scale='c'
            temperature={celsius}
            onTemperatureChange={handleCelsiusChange}/>
            <TemperatureInput
            scale='f'
            temperature={fahrenheit}
            onTemperatureChange={handleFahrenheitChange}/>
            <BoilingVerdict celsius={parseFloat(celsius)}/>
        </div>
    )
}

export default Calculator;

 

 

앞에서 만든 TemperatureInput 컴포넌트를 사용해서 섭씨, 화씨 두가지 입력 양식을 제공

 

모든 온도를 섭씨로 변환해서 BoilingVerdict 컴포넌트에 전달해서, 물이 끓는지 아닌지를 출력

 

이제 화면에 렌더링하기 위해 index.js 파일을 수정

 

import Calculator from './chapter_12/Calculator';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Calculator />
  </React.StrictMode>
);

 

npm start로 실제 실행

 

섭씨 온도쪽에 99 입력해보면 아래 화씨에 자동으로 변환됨

 

비슷하게 화씨쪽에 259를 입력하면 자동으로 섭씨온도도 변환되어 출력

 

TAGS.

Comments