본문 바로가기
웹/24-StudyWithPnP

[React.js] 4주차 - state, useState, useRef, React Hooks, 렌더링

by 보먀 2024. 10. 29.
728x90
반응형

1. State 

State 는 현재 가지고 있는 형태나 모양을 정의하고, 변화할 수 있는 동적이 값을 말한다. 

 

리액트에서 모든 컴포넌트들은 State 를 가질 수 있는데(한 개의 컴포넌트는 여러개의 State 를 가질 수 있음), State 를 갖는 컴포넌트들은 이 State 값에 따라 렌더링 되는 UI 가 결정된다. 이 때 컴포넌트의 State 값에 따라서 화면이 다시 렌더링 되는 것을 리 렌더(Re-Render) 또는 리 렌더링(Re-Rendering) 이라고 한다. 

 

 

1.1. useState 사용하기

 

useState 를 사용하려면 리액트의 내장 함수인 useState 를 import 해줘야 한다. 

import { useState } from 'react';

 

 

useState 함수를 호출하고 반환값을 state 함수에 담아 출력해보면 useState 가 2개의 요소를 담은 배열을 반환하는 것을 확인할 수 있다. 첫번째 요소는 undefined 로 반환이 되고, 두번째 요소는 함수가 반환이 된다. 

 

useState 반환값의 첫번째 요소는 새롭게 생성된 state 값이고, 두번째 요소는 상태를 변화시키는 상태변화함수이다. 

const state = useState();
console.log(state);

 

 

그래서 useState 인자로 값을 넣어주면 state 의 초기값을 설정할 수 있다. 

const state = useState(1);
console.log(state);

 

 

그래서 보통은 디스트럭처링 문법을 사용해서 이렇게 사용한다. 그래서 num 에는 초기값으로 할당해준 1이 들어있고, setNum 에는 상태값을 변경시키는 상태 변경 함수가 들어오게 된다. 

const [state, setState] = useState(1);

 

 

이제 버튼을 누르면 화면에 숫자가 증가하는 코드를 작성해보자.

 

컴포넌트 내에 새로운 state 를 생성하고 해당 state 의 값을 변경해주게 되면 리액트가 내부적으로 App 컴포넌트의 state 가 변경되었다는 것을 감지하고 이 컴포넌트를 리렌더링 시켜준다. 쉽게 말해 컴포넌트 리렌더링은 컴포넌트의 state 값이 바뀌면 컴포넌트(함수)가 변경된 state 값을 반영해서 리턴하고 그 값으로 다시 화면을 그린다고 생각하면 된다. 

import './App.css'
import { useState } from 'react';

function App() {
  const [count, setCount] = useState(1);

  return (
    <>
      <h1>{ count }</h1>
      <button onClick={() =>{
        setCount(count+1);
      }}>
        +
      </button>
    </>
  );
}

export default App;

 

 

1.2. state 를 props 로 전달하기

 

 

Bulb 컴포넌트는 부모인 App 컴포넌트로부터 light 라는 state 값을 받아 값에 따라 배경을 설정해준다.

여기서 중요한 점은 자식 컴포넌트가 자체적으로 가지는 state 값이 변경되지 않아도 부모로부터 받는 props 의 값이 바뀌면 리렌더링이 발생한다는 것이다. 

import './App.css'
import { useState } from 'react';

const Bulb = ({light}) => {
  console.log(light);
  return <div>
    {light === 'ON' ? (
      <h1 style={{backgroundColor: "orange"}}>ON</h1>
    ) : (
      <h1 style={{backgroundColor: "gray"}}>OFF</h1>
    )
  }
  </div>
};

function App() {
  const [count, setCount] = useState(1);
  const [light, setLight] = useState('OFF');

  return (
    <>
      <div>
        <Bulb light={light} />
        <button onClick={() => {
          setLight(light === 'ON' ? 'OFF' : 'ON');
        }}>
          {light === 'ON' ? '끄기' : '켜기'}
        </button>
      </div>
      <div>
        <h1>{ count }</h1>
        <button onClick={() =>{
          setCount(count+1);
        }}>
          +
        </button>
      </div>
    </>
  );
}

export default App;

 

 

그런데 이상한 점을 발견할 수 있다. Bulb 컴포넌트에서 console.log 로 부모 컴포넌트로부터 받은 light state 값을 출력하고 있는데, 켜기 버튼을 누르는 것이 아니라 + 버튼을 눌렀을 때도 Bulb 컴포넌트 안에 있는 출력문이 찍히고 있다. 

 

먼저 리액트가 어떤 상황에서 렌더링이 일어나는지 알아보자.

 

리액트에서 리렌더링이 일어나는 상황 3가지

 

1. 자신이 관리하는 state 의 값이 변경될 때

2. 자신이 제공받는 props 의 값이 값이 변경되었을 때

3. 부모 컴포넌트가 리렌더링이 되면 자식 컴포넌트도 계속 리렌더링됨

 

3번에 따라 리액트는 부모 컴포넌트가 리렌더링이 일어났을 때 자식 컴포넌트도 리렌더링이 일어나는데 현재 + 버튼이 눌려져서 부모 컴포넌트인 app 컴포넌트에서 리렌더링이 일어나기 때문에 자식 컴포넌트인 Bulb 컴포넌트도 리렌더링이 일어났기 때문에 출력문이 계속 찍히는 것이다. 

 

 

 

근데 Bulb 컴포넌트의 리렌더링은 count 와는 아무 상관이 없기 때문에 쓸데없는 성능 저하를 불러일으킬 수 있다. 그래서 이런 경우를 방지하기 위해서는 관련없는 state 들은 다른 컴포넌트로 분리해주는 것이 좋다. 

-> count 와 관련된 기능 / light 와 관련된 기능 이렇게 기능을 분리해서 다른 컴포넌트로 옮김

// Bulb.jsx

import { useState } from "react";

const Bulb = () => {
    const [light, setLight] = useState('OFF');
    console.log(light);
    
    return (
      <div>
        {light === 'ON' ? (
          <h1 style={{backgroundColor: "orange"}}>ON</h1>
        ) : (
          <h1 style={{backgroundColor: "gray"}}>OFF</h1>
        )
        }
        <button onClick={() => {
                setLight(light === 'ON' ? 'OFF' : 'ON');
              }}>
                {light === 'ON' ? '끄기' : '켜기'}
        </button>
      </div>
    );
  };

  export default Bulb;
// Counter.jsx

import { useState } from "react";

const Counter = () => {
    const [count, setCount] = useState(1);
  
    return (
      <div>
        <h1>{ count }</h1>
        <button onClick={() =>{
          setCount(count+1);
        }}>
          +
        </button>
      </div>
    );
  };

  export default Counter;
// App.jsx

import './App.css'
import { useState } from 'react';
import Bulb from './components/Bulb';
import Counter from './components/Count';

function App() {

  return (
    <>
      <Bulb />
      <Counter />
    </>
  );
}

export default App;

 

 

 

2. 사용자 입력받기

 

- input

<div>
    <input
        value={name}
        onChange={onChangeName}
        placeholder={"이름"}
    />
</div>
<div>
    <input
        type="date"
        value={birth}
        onChange={onChangeBirth}
        placeholder={"생일"}
    />
</div>

 

 

- select 

 

select 박스의 기본 값은 가장 위의 option 값으로 설정되기 때문에 아무 값도 기본값으로 설정하고 싶지 않다면, 맨위에 아무것도 넣지 않은 option 태그를 넣어주면 된다. 또 select 박스는 선택지와 실제 코드 상에서 사용할 value 값을 다르게 선택할 수 있다. 

<select onChange={onChangeCountry}>
    <option value=''></option>
    <option value='kr'>한국</option>
    <option value='us'>미국</option>
    <option value='us'>영국</option>
</select>

 

 

- textarea

 

input 태그와 유사하지만, 여러줄을 입력할 수 있다. 

<div>
    <textarea onChange={onChangeBio} />
    {bio}
</div>

 

 

전체 코드

import { useState } from "react";

function Register() {

    const [name, setName] = useState("이름");
    const [birth, setBirth] = useState("");
    const [country, setCountry] = useState("");
    const [bio, setBio] = useState("");

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

    const onChangeBirth = (e) => {
        setBirth(e.target.value);
    }

    const onChangeCountry = (e) => {
        setCountry(e.target.value);
    }

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

    return (
    <div>
        <div>
            <input
            value={name}
            onChange={onChangeName}
            placeholder={"이름"}
            />
        </div>
        <div>
            <input
            type="date"
            value={birth}
            onChange={onChangeBirth}
            placeholder={"생일"}
            />
        </div>
        <div>
            <select value={country} onChange={onChangeCountry}>
                <option value=''></option>
                <option value='kr'>한국</option>
                <option value='us'>미국</option>
                <option value='us'>영국</option>
            </select>
        </div>
        <div>
            <textarea value={bio} onChange={onChangeBio} />
            {bio}
        </div>
    </div>
    );
  }

export default Register;

 

 

사실 위의 코드에서는 비슷한 코드들이 반복되기 때문에 조금 더 효율적으로 코드를 짜기 위해 아래의 방법들을 사용했다. 

 

1. state를 객체 형태로 묶기

2. onChange 함수 핸들러를 한 개로 만들기

import { useState } from "react";

function Register() {

    const [input, setInput] = useState({
        name: "",
        birth: "",
        country: "",
        bio: ""
    });

    const onChange = (e) => {
        setInput({
            ...input,
            [e.target.name]: e.target.value
        });
    };

    return (
    <div>
        <div>
            <input
            name="name"
            value={input.name}
            onChange={onChange}
            placeholder={"이름"}
            />
        </div>
        <div>
            <input
            name="birth"
            value={input.birth}
            onChange={onChange}
            placeholder={"생일"}
            type="date"
            />
        </div>
        <div>
            <select name="country" value={input.country} onChange={onChange}>
                <option value=''></option>
                <option value='kr'>한국</option>
                <option value='us'>미국</option>
                <option value='us'>영국</option>
            </select>
        </div>
        <div>
            <textarea name="bio" value={input.bio} onChange={onChange} />
            {input.bio}
        </div>
    </div>
    );
  }

export default Register;

 

 

 

3. useRef

 

새로운 Reference 객체를 생성하는 기능으로 컴포넌트 내부의 변수로써 일반적인 값들을 저장할 수 있다. 

 

state 와 비슷해보이지만, 값이 변경되었을 때 컴포넌트를 리렌더링 시키는 useState 와는 달리 useRef 로 생성한 변수는 값이 변경되더라도 컴포넌트를 리렌더링 시키지는 않는다. 그렇기 때문에 렌더링에 영향을 미치고 싶지 않은 변수를 생성할 때 useRef 를 이용한다. 

 

또 useRef 를 이용하면 컴포넌트가 렌더링하는 특정 DOM 요소에 접근할 수 있기 때문에 해당 요소를 조작할 수 있다. 

(ex. 특정 요소 focus, 요소의 스타일 변경 등)

 

useRef 는 값이 변해도 리렌더링되지 않기 때문에 refObj 는 처음 렌더링될 때 1번 실행되어 'register 렌더링' 을 출력한 후에 다시 출력되 않는 것을 확인할 수 있다.

import { useState, useRef } from "react";

function Register() {

    const refOjb = useRef(0);
    console.log('register 렌더링');

    return (
    <div>
        <button onClick={() => {
            refOjb.current++;
            console.log(refOjb.current);
        }}>
            ref + 1
        </button>
    </div>
    );
  }

export default Register;

 

 

또 useRef 로 포커싱을 구현할 수 있다. 

inputRef 를 input 태그의 ref 속성에 연결해서 input 요소에 직접 접근하여 포커싱을 수행할 수 있다. 

 

onSubmit 함수에서 input.name 이 비어있을 경우 inputRef.current.focus() 를 호출 -> input 요소에 포서스 설정

inputRef 가 input 요소의 참조를 가리키고 있기 때문에 inputRef.current 는 해당 input DOM 요소가 된다. 리렌더링과 상관없이 특정동작을 수행할 수 있기 때문에 효율적으로 동작할 수 있다. 

const inputRef = useRef();

// ...

const onSubmit = () => {
    if (input.name === "") {
        inputRef.current.focus();
    }
}

return (
    <div>
        <input
            ref={inputRef}
            name="name"
            value={input.name}
            onChange={onChange}
            placeholder={"이름"}
        />
    </div>
);

 

 

정리하자면,

  • 렌더링할 때마다 재설정되는 일반 변수와 달리 리렌더링 사이에 정보를 저장할 수 있다
  • 리렌더링을 촉발하는 state 변수와 달리 변경해도 리렌더링을 촉발하지 않는다
  • 정보가 공유되는 외부변수와 달리 각각의 컴포넌트에 로컬로 저장된다

 

※ 참고 - useRef 와 자바스크립트 일반 변수의 다른 점?

 

리렌더링이 되지 않는다면, 자바스크립트 일반 변수를 사용하는 것과 같지 않을까? 라는 생각을 할 수 있다. 코드를 보면서 어떤지 알아보자.

 

아래의 코드를 돌려보면, count 가 1까지 밖에 증가되지 않는 것을 확인할 수 있다. 

변화를 감지하고 onChange 함수가 실행되면, 컴포넌트가 재렌더링되어 일반 변수인 count 가 다시 0으로 초기화 되기 때문에 제대로 동작하지 않는다.

const inputRef = useRef();

let count = 0;

const onChange = (e) => {
    count++;
    console.log(count);
    setInput({
        ...input,
        [e.target.name]: e.target.value
    });
};

 

 

그렇다면 count 함수를 컴포넌트 밖으로 뺀다면 제대로 동작하지 않을까? 결론부터 말하면 제대로 동작하는 것 같지만 제대로 동작하지 않는다. 

 

이렇게 같은 컴포넌트 두 개를 사용하는 경우 문제가 발생한다. 컴포넌트 즉, Register 함수가 2번 실행이 되는데 컴포넌트 밖에 있는 count 변수는 한 개이므로 두 개의 함수가 공유를 하는 상황이 되기 때문에 치명적이 오류를 발생시킬 수 있다. 

(함수만 두 번 호출이 되어서 두 개의 컴포넌트가 하나의 변수를 공유하고 있는 것)

// App.jsx

import './App.css'
import Register from './components/Register';

function App() {

  return (
    <>
      <Register />
      <Register />
    </>
  );
}

export default App;
// Register.jsx

let count = 0;

function Register() {

    const [input, setInput] = useState({
        name: "",
        birth: "",
        country: "",
        bio: ""
    });

    const inputRef = useRef();

    const onChange = (e) => {
        // countRef.current++;
        // console.log(countRef.current);
        count++;
        console.log(count);
        setInput({
            ...input,
            [e.target.name]: e.target.value
        });
    };

    const onSubmit = () => {
        if (input.name === "") {
            inputRef.current.focus();
        }
    }
    // ...
  }

 

 

 

4. React Hooks

 

2017년 이전 리액트에서 함수 컴포넌트는 단순히 UI 를 렌더링하는 것 외에는 아무런 기능도 쓸 수 없었기 때문에 대부분 클래스 컴포넌트를 사용했다. 근데 클래스 컴포넌트는 클래스 문법을 사용해야 했기 때문에 함수 컴포넌트에 비해 코드가 굉장히 복잡해 많은 사람들이 함수 컴포넌트에서 모든 기능을 이용할 수 있기를 바랬다. 그 결과 함수 컴포넌트에서도 클래스 컴포넌트의 기능을 가져와 사용할 수 있게 해주는 리액트 훅이라는 기능이 개발되었다. 

 

앞서 배웠던 useState, useRef 모두 리액트 훅인데, 리액트 훅은 이처럼 이름 앞에 접두사 use 가 붙는 특징이 있다. 

-> useState: State 기능을 낚아채오는 Hook

-> useRef: Reference 기능을 낚아채오는 Hook

(이 외에도 useEffect, useReducer 등의 다양한 훅들이 있다)

 

 

4.1. 리액트 훅 사용하기

 

리액트 훅을 사용하는데는 몇가지 규칙이 존재한다.

 

1. 리액트 훅들은 함수 컴포넌트 내부/커스텀 훅 내부에서만 호출될 수 있다.

 

함수 컴포넌트 외부에서 훅을 사용하려고 하면 에러가 발생한다. 

 

 

2. 조건부로 호출되어서는 안된다 -> 조건문이나 반복문에서 호출이 불가능

 

조건문이나 반복문 내부에서 훅을 호출하게 되면 서로 다른 훅들의 호출 순서가 엉망이 되어버리는 현상이 발생해서 내부적인 오류가 발생한다. 

 

3. use 라는 접두사를 이용해서 직접 나만의 새로운 훅을 커스텀해서 사용할 수 있다.

 

컴포넌트 밖에 getInput 이라는 일반 함수를 만들어서 커스텀 훅으로 사용하려고 했더니, 리액트 훅은 컴포넌트 안에서 밖에 사용할 수 없다는 규칙 때문에 에러가 발생한다. 이때 get 대신 use 라는 접두사를 사용하면 리액트에서 내부적으로 함수를 커스텀 훅이라고 판단하게 되어 또 다른 리액트 훅을 일반 함수 내부에서 호출해도 에러를 발생시키지 않는다. 

 

 

컴포넌트마다 반복되어서 동작하는 로직이 있고 해당 로직이 useState 같은 훅을 사용하는 로직이라면 그러한 로직은 커스텀 훅을 만들어서 분리해 줄 수 있다. 분리해서 만든 커스텀 훅은 반복해서 사용이 가능하다. 

const [input, setInput] = useInput();
const [input2, setInput2] = useInput();

 

그리고 사실 커스텀 훅은 컴포넌트랑 같은 파일에 두기 보다는 분리해서 src 디렉토리 아래 hooks 라는 디렉토리를 만들어서 그 안에 보관하는 것이 일반적이다. 

 

 

 

728x90
반응형