React 재활훈련- 4일차, Event Handling, State, Ref

0

react

목록 보기
4/11

https://www.udemy.com/course/react-next-master/

React basic

Event handling

Event란 웹에서 발생하는 사용자의 행동들을 말한다. 가령 버튼을 누르거나 메세지를 입력하는 것들이 있다. 따라서, Event handling이란 이벤트가 발생한 것을 어떻게 처리할 것인가?이다.

웹에서 발생하는 사용자의 행동에 따라 어떻게 처리할 지 결정하는 것이다.

button이 눌리면 경고창이 나오도록 해보자.

  • components/Button.jsx
import './Button.css'

export default function Button({text, color, children}) {
    return (
        <button 
            className="button"
            style={{backgroundColor: color}}
            onClick={() => {
                alert("버튼을 클릭했습니다.")
            }}    
        >
            {children}
        </button>
    ) 
}

onClick안에 함수를 하나 만들어주면 된다. 이 함수가 바로 event handler가 되는 것이다. 다음처럼 arrow function을 바로 써줄 수도 있지만 따로 함수를 만들어 사용할 수도 있다.

  • components.Button.jsx
import './Button.css'

export default function Button({text, color, children}) {
    const onClick = () => {
        alert("버튼을 클릭했습니다.")
    }    
    return (
        <button 
            className="button"
            style={{backgroundColor: color}}
            onClick={onClick}    
        >
            {children}
        </button>
    ) 
}

단, onClick과 같은 event에 event handler 함수를 전달할 때는 함수 이름을 전달하는 것이지 실행한 것을 전달하면 안된다. 즉, onClick()으로 전달하면 안된다.

이렇게 event handler를 전달하면 event handler 함수 인자에 event객체를 매개변수로 넣어준다.

  • components/Button.jsx
import './Button.css'

export default function Button({text, color, children}) {
    const onClick = (e) => {
        console.log(e)
    }    
    return (
        <button 
            className="button"
            style={{backgroundColor: color}}
            onClick={onClick}    
        >
            {children}
        </button>
    ) 
}

onClick event handler 함수에 들어간 매개변수 e가 바로 event객체이다. console.log로 찍어보면 다음을 알 수 있다.

SyntheticBaseEvent {_reactName: 'onClick', _targetInst: null, type: 'click', nativeEvent: PointerEvent, target: div,}

SyntheticBaseEvent는 말그대로 합성 이벤트 객체라고 하는데, 사실 웹브라우저는 정말 다양하고, 이들의 동작 방식이 서로 다르다. 브라우저마다의 동작 방식이 다르니, event들도 다를 수 밖에 없었다. 가령, chrome의 경우 현재 event가 발생한 요소를 target이라 하지만, 사파리에서는 ETarget이라 할 수도 있다.

이렇게 브라우저마다 동작 방식이 달라 발생하는 이슈를 cross browing issue라고 한다. 이를 해결해주는 것이 바로 합성 이벤트 객체이다. 이는 모든 웹 브라우저의 event를 하나의 객체로 합성해놓은 것이다. 따라서 개발자들은 이 통합 규격 이벤트 객체를 통해서, 하나의 code로 모든 브라우저를 호환할 수 있다.

State

state는 상태를 말한다. 상태는 어떤 사물의 모양이나 동작을 말한다. 추가적으로 상태는 계속 변화하는 값이라고도 한다.

react의 component도 상태를 가지게 되는데, 이 변화한 상태에 따라 component의 모습을 바꾸어 렌더링된다. 가령, 전구가 off 상태이면 빛이 안켜지지만 on으로 상태가 변한다면 빛이 켜지는 모습으로 다시 렌더링된다. 따라서 state의 변화는 rerendering을 호출한다고 볼 수 있다

state를 생성하는 방법은 특별한 함수인 reactuseState를 사용해야한다. 따라서 다음의 import문이 필요하다.

import { useState } from "react";

useState를 사용하는 방법은 매우 간단한데, useState함수에 state의 초기값을 넣어주면 된다. 그러면 return value로 array를 반환하는데, array의 첫번째 값은 state이고, 두번째 값은 state의 값을 설정하는 setter이다.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [light, setLight] = useState("OFF")
    console.log(light)

    return (
        <div className="body">
            {light}    
        </div>
    )
}

export default Body

다음과 같이 배열의 구조분해할당으로 값을 가져와 줄 수 있다.

정리하자면 light라는 state가 나오고 setLight를 통해서 light의 값을 변경할 수 있다. 이를 상태변화함수라고 한다.

console.log(light)로 확인하면 OFF가 나오는 것을 볼 수 있고, javascript 변수처럼 light state가 rendering되는 것 또한 볼 수 있다.

이제 setLight를 통해서 light state를 변경해보도록 하자.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [light, setLight] = useState("OFF")
    console.log(light)

    return (
        <div className="body">
            {light}
            <button onClick={() => {
                setLight("ON")
            }}>
                켜기
            </button>
        </div>
    )
}

export default Body

버튼을 누르면 setLight가 실행되어 lightOFF에서 ON으로 바뀐다. 재밌는 것은 렌더링도 다시되어 화면에 보이는데, 이는 statelightON으로 변경되었고 react에서 이를 감지하여 리렌더링했다는 것이다.

다시 렌더링이 실행된다는 것은 다른 말로 함수를 다시 실행시킨다는 말과 같다. 따라서, Body react component 전체가 다시 실행되어 console.log 값이 또 찍히게 될 것이다.

그런데, javascript의 변수로 동일한 logic을 만들면 되지 않는가? 그러나 일반 javascript 변수는 state가 아니므로 변수값이 바뀌어도 rerendering이 발생하지 않는다.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    let light = "OFF"

    return (
        <div className="body">
            {light}
             <button onClick={() => {
                light = "ON"
             }}>
                켜기
             </button>
        </div>
    )
}

export default Body

버튼을 아무리 눌러도 화면이 바뀌지 않을 것이다. 이는 light가 더이상 state가 아니고 일반 javascript 변수이기 때문이다. 따라서, light값이 바뀌어도 리렌더링이 발생하지 않는다.

State와 Props

이제 state와 props를 함께 사용해보도록 하자. state를 props로 쓸 수 있는데, 자세히 말해보자면 부모의 state를 자식에게 props로 내려줄 수 있다는 것이다.

이를 위해서 다음과 같이 code를 만들어보도록 하자.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function LightBulb({ light }) {
    return <>
        {light === "ON" ? <div style={{ backgroundColor: "orange"}}>ON</div> 
        : <div style={{ backgroundColor: "gray"}}>OFF</div>}
    </>
}

function Body() {
    const [light, setLight] = useState("OFF")

    return (
        <div className="body">
            <LightBulb light={light}/>
            <button onClick={() => {
                setLight("ON")
            }}>
                켜기
            </button>

            <button onClick={() => {
                setLight("OFF")
            }}>
                끄기
            </button>
        </div>
    )
}

export default Body

LightBulb는 자식 component로 light를 props로 받아 렌더링을 해준다. 이 light props를 내려주는 부모 component인 Body는 state로 light를 가지고 있고, 이를 LightBulb props에 넣어주는 것이다.

이렇게 구성하면 끄기, 켜기 버튼을 번갈아 누를 때마다 LightBulb가 리렌더링된다. 즉, light state가 바뀔 때마다, 부모인 Body component와 자식인 LightBulb component가 리렌더링된다는 것이다. 이를 통해 알 수 있는 것은 state가 변하면 state를 가진 component는 리렌더링된다는 것이고, 부모로 부터받은 propsstate이고 이 값이 변하면 자식 component도 리렌더링된다는 것이다.

한 가지 재밌는 실험을 할 수 있는데, 다음의 예제를 보도록 하자.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function LightBulb({ light }) {
    return <>
        {light === "ON" ? <div style={{ backgroundColor: "orange"}}>ON</div> 
        : <div style={{ backgroundColor: "gray"}}>OFF</div>}
    </>
}

function StaticLightBulb() {
    console.log("STATIC LIGHT BULB")
    return <div style={{ backgroundColor: "gray"}}>OFF</div>
}

function Body() {
    const [light, setLight] = useState("OFF")

    return (
        <div className="body">
            <LightBulb light={light}/>
            <StaticLightBulb/>
            <button onClick={() => {
                setLight("ON")
            }}>
                켜기
            </button>

            <button onClick={() => {
                setLight("OFF")
            }}>
                끄기
            </button>
        </div>
    )
}

export default Body

StaticLightBulb는 부모로 부터 state props를 받지도 않았고, 자체 state도 없으니 light state가 변해도 안변하는 것이 맞다. 그런데 정말일까? 켜기, 끄기 버튼을 여러 번 누르고 console을 확인하면 다음을 볼 수 있다.

STATIC LIGHT BULB
STATIC LIGHT BULB
STATIC LIGHT BULB
STATIC LIGHT BULB

계속해서 리렌더링이 되는 것을 볼 수 있다. 이는 react에서 부모 component가 리렌더링되면 자식 component도 리렌더링된다는 것을 알 수 있다.

따라서, 부모 component가 리렌더링되면 자식 component 모두 리렌더링된다는 것이다. 이렇게되면 web의 성능이 매우 안좋아진다.

이러한 불필요한 리렌더링이 발생하지 않도록 부모-자식 component 관계를 최적화를 하는 것이 좋다.

State로 사용자 입력 관리하기

먼저 input tag를 만들어 사용자의 입력을 받도록 하자.

  • components/Body.jsx
import "./Body.css"

function Body() {
    return (
        <div className="body">
            <input />
        </div>
    )
}

export default Body

이제 input tag에 들어갈 value를 state로 다루고 싶다. valuestate로 다루어 Body component에서 사용자의 입력으로 들어온 value를 통해 로직을 부여하고, 제어하고 싶은 것이다.

그러나, 다음과 같이 inputvaluestate로 넣어주면 input tag가 동작하지 않은 것을 볼 수 있다.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [name, setName] = useState("");

    return (
        <div className="body">
            <input value={name} />
        </div>
    )
}

export default Body

아무리 input html tag에 값을 입력해도 값이 써지지 않는 것을 볼 수 있다. 왜냐하면 name state의 초기값인 ""가 고정으로 들어가 input tag의 value가 변하지 않기 때문이다.

따라서, 사용자의 입력에 따라 name state에 input의 value가 들어가도록 event handler를 만들어주어야 한다.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [name, setName] = useState("");

    const onChnageName = (e) => {
        setName(e.target.value)
    }

    return (
        <div className="body">
            <input value={name} onChange={onChnageName} />
        </div>
    )
}

export default Body

이제 input tag에 값을 입력하면, name state에 반영이 될 것이다. e.target.value가 바로 사용자가 입력한 input value값이기 때문이다. 즉, input tag에 사용자 입력이 들어오면 onChange event handler가 동작하고, 이에 따라 name state의 값이 사용자의 입력인 e.target.value로 바뀐다.

input tag말고도 select tag도 사용해보도록 하자.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [name, setName] = useState("");
    const [gender, setGender] = useState("")

    const onChnageName = (e) => {
        setName(e.target.value)
    }

    const onChangeGender = (e) => {
        setGender(e.target.value)
    }

    return (
        <div className="body">
            <div>
                <input value={name} onChange={onChnageName} />
            </div>
            <div>
                <select value={gender} onChange={onChangeGender}>
                    <option value="">밝히지 않음</option>
                    <option value="female">여성</option>
                    <option value="male">남성</option>
                </select>
            </div>
        </div>
    )
}

export default Body

방법이 크게 다르지 않다. select tag에 valuestate값인 gender를 넣어주고 onChangesetGender를 호출하여 gender값을 바꾸면 된다.

다음으로 textarea tag도 동일한 방법으로 state를 설정할 수 있다.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [name, setName] = useState("");
    const [gender, setGender] = useState("")
    const [bio, setBio] = useState("")

    const onChnageName = (e) => {
        setName(e.target.value)
    }

    const onChangeGender = (e) => {
        setGender(e.target.value)
    }

    const onChangeBio = (e) => {
        setBio(e.target.value)
    }

    return (
        <div className="body">
            <div>
                <input value={name} onChange={onChnageName} />
            </div>
            <div>
                <select value={gender} onChange={onChangeGender}>
                    <option value="">밝히지 않음</option>
                    <option value="female">여성</option>
                    <option value="male">남성</option>
                </select>
            </div>
            <div>
                <textarea value={bio} onChange={onChangeBio} />
            </div>
        </div>
    )
}

export default Body

별반 다를바 없는 것을 볼 수 있다.

그런데, 사용자에게 입력을 받는 form이 늘어날 때마다, 이 과정을 계속 반복해야하는 것인가?? form이 너무 많아지면 어떻게해야할까?? 너무 많은 state와 event handler가 있을 것이고, 동일한 로직을 가진 event handler로 범벅이 되어있을 것이다. 즉, redundancy가 너무 많아지는 것이다.

이를 해결하기 위한 방법으로 통합 state를 구축해서 해결해보도록 하자.

  • components/Body.jsx
import "./Body.css"
import { useState } from "react";

function Body() {
    const [state, setState] = useState({
        name: "",
        gender: "",
        bio: ""
    })

    const onChange = (e) => {
        setState({
            ...state,
            [e.target.name]: e.target.value
        })
    }

    return (
        <div className="body">
            <div>
                <input name={"name"} value={state.name} onChange={onChange} />
            </div>
            <div>
                <select name={"gender"} value={state.gender} onChange={onChange}>
                    <option value="">밝히지 않음</option>
                    <option value="female">여성</option>
                    <option value="male">남성</option>
                </select>
            </div>
            <div>
                <textarea name={"bio"} value={state.bio} onChange={onChange} />
            </div>
        </div>
    )
}

export default Body

방법은 그렇게 어렵지 않다. state를 만들되 state는 object로 다음의 property를 가진다.

{
    name: "",
    gender: "",
    bio: ""
}

state의 property를 각 input, select, textareavalue로 넣어주도록 하는 것이다.

다음으로 onChange event handler인데, 이 부분이 약간 tricky하다.

const onChange = (e) => {
        setState({
            ...state,
            [e.target.name]: e.target.value
        })
    }

setState에 새로운 object를 넣는데, ...state spread문법으로 기존 state값들을 넣어준다. 다음으로 변경된 state값을 넣어주어야하는데, 문제는 object의 key값을 어떤 값으로 넣어주어야 하는 지 모른다는 것이다.

그래서 e.target.name으로 설정했는데, 이 값이 바로 input, select, textareaname property값이다.

<div className="body">
    <div>
        <input name={"name"} value={state.name} onChange={onChange} />
    </div>
    <div>
        <select name={"gender"} value={state.gender} onChange={onChange}>
            ...
        </select>
    </div>
    <div>
        <textarea name={"bio"} value={state.bio} onChange={onChange} />
    </div>
</div>

input, select, textarea html tag 각각에 name property가 있는 것을 볼 수 있다. 이 값을 onChnage event handler에서 가져와 object의 key값으로 써주는 것이다. 그게 [e.target.name]이다.

왜 대괄호를 치냐고하냐면, 이것이 js문법이기 때문이다. js에서는 object의 property key로 바로 변수값을 넣어줄 수 없다. 때문에 변수를 object의 key로 쓰기위해서는 []를 변수에 넣어주어야 한다.

이 덕분에 하나의 stateeventHandler를 통해서 여러 개의 입력 form들을 제어할 수 있는 것이다.

Ref

react에서 특정 DOM에 접근하기 위해서는 ref를 사용해야한다. 이때 사용하는 react함수가 useRef이다. ref 객체는 component가 리렌더링되어도 그대로 유지된다. 따라서, 특정 DOM을 참조하기 위해서 많이 사용된다.

만약 button을 눌렀을 때, name input이 비어있다면 해당 DOM으로 focus가 가도록 하고 싶다고 하자. 다음과 같이 만들 수가 있다.

import "./Body.css"
import { useState, useRef } from "react";

function Body() {
    const [state, setState] = useState({
        name: "",
        gender: "",
        bio: ""
    })

    const onChange = (e) => {
        setState({
            ...state,
            [e.target.name]: e.target.value
        })
    }

    const nameRef = useRef()

    const onSubmit = () => {
        if (state.name === "") {
            nameRef.current.focus()
            return
        }
    }

    return (
        <div className="body">
            <div>
                <input ref={nameRef} name={"name"} value={state.name} onChange={onChange} />
            </div>
            <div>
                <select name={"gender"} value={state.gender} onChange={onChange}>
                    <option value="">밝히지 않음</option>
                    <option value="female">여성</option>
                    <option value="male">남성</option>
                </select>
            </div>
            <div>
                <textarea name={"bio"} value={state.bio} onChange={onChange} />
            </div>
            <div>
                <button onClick={onSubmit}>회원가입</button>
            </div>
        </div>
    )
}

export default Body

먼저 주목해야할 것은 useRef이다. useRef를 통해서 reference를 만들고 이 reference를 inputref에 넣어서 input DOM의 reference를 주입해주는 것이다.

0개의 댓글