useState, useEffect, useRef, useContext

Yeji·2023년 9월 2일
0

유투브 강의React 공식문서를 기반으로 학습한 내용이다.

1. useState

1-1. 정의

컴포넌트의 상태를 생성하고 업데이트 시켜주는 도구

state에 정의된 값이 바뀔 때마다 재렌더링을 진행한다.

1-2. 기본

이름을 제출할 때마다 names라는 state을 업데이트 시키는 작업을 진행한다.

현재 names의 초기값은 ['홍길동', '정예지']다.
그런데 names의 초기값을 불러오는데 많은 양의 작업과 시간이 소모된다면, setNames를 호출할 때마다 해당 작업을 반복하게 되어 성능상 매우 불리해질 것이다.

import { useState } from 'react'

function App() {
  const [names, setNames] = useState(['홍길동', '정예지'])
  const [name, setName] = useState('')

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

  const handleOnClick = () => {
    setNames((prevState) => {
      return [...prevState, name]
    })
    setName('')
  }

  return (
    <div>
      <input type="text" value={name} onChange={handleOnChange} />
      <button onClick={handleOnClick}>ADD</button>
      {names.map((name) => {
        return <div>{name}</div>
      })}
    </div>
  )
}

export default App

1-3. 활용

이 때는 useState에 callback 함수를 사용해 해결할 수 있다.
다음과 같이 useState을 선언하면 맨 처음 렌더링 될 때만 해당 작업을 실행하게 된다.

import { useState } from 'react'

const heavywork = () => {
  return ['홍길동', '정예지']
}

function App() {
  // callback 함수로 초기값 할당
  const [names, setNames] = useState(() => {
    return heavywork()
  })
  const [name, setName] = useState('')

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

  const handleOnClick = () => {
    setNames((prevState) => {
      return [...prevState, name]
    })
    setName('')
  }

  return (
    <div>
      <input type="text" value={name} onChange={handleOnChange} />
      <button onClick={handleOnClick}>ADD</button>
      {names.map((name) => {
        return <div>{name}</div>
      })}
    </div>
  )
}

export default App

2. useEffect

2-1. 정의

컴포넌트 마운트, 업데이트(제렌더링), 언마운트시 특정 작업을 실행하는 도구

2-2. 기본

useEffect에 전달된 콜백함수는 첫 렌더링시, 혹은 dependency 배열의 값이 변경될 때마다 실행된다.

언마운트시 혹은 렌더링 직전에 특정 로직을 실행하고 싶다면 cleanup 함수를 이용할 수 있다.

useEffect(() => {
    console.log('마운트시')

    return () => { // cleanup
      console.log('언마운트시 혹은 업데이트 직전')
    }
  }, []) // dependency array

2-3. 활용

1초마다 콘솔에 값을 출력하는 타이머를 만들어보자.

cleanup 함수를 이용하지 않으면 Timer 컴포넌트가 언마운트가 되어도 이전에 선언한 타이머가 계속해서 동작한다.

따라서 cleanup 함수에 이전 타이머를 종료시키는 작업을 추가해야 정상적으로 동작한다.

// Timer.js

import React, { useEffect } from 'react'

export default function Timer() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('타이머 시작')
    }, 1000)

    return () => {
      clearInterval(timer)
      console.log('타이머 종료')
    }
  }, [])
  return <div>타이머를 시작합니다</div>
}
// App.js

import { useState } from 'react'
import Timer from './Timer'

function App() {
  const [show, setShow] = useState(false)

  return (
    <div>
      {show && <Timer />}
      <button onClick={() => setShow(!show)}>Toggle</button>
    </div>
  )
}

export default App

2-4. dependency array

그럼 useEffect dependency array가 있는 것, 없는 것, 비어있는 것은 각각 어떻게 동작할까?

공식문서에 아주 자세하게 설명되어 있다.

dependency array

첫 렌더링 때 동작하고 dependency array 값이 변할 때 재렌더링시 동작한다.

useEffect(() => {
  // ...
}, [a, b]); // Runs again if a or b are different

empty dependency array

아무 값도 넣어주지 않으면 첫 렌더링 때만 동작한다.

useEffect(() => {
  // ...
}, []); // Does not run again (except once in development)

no dependency array

dependency array가 아예 없다면 모든 렌더링 때마다 동작한다.

useEffect(() => {
  // ...
}); // Always runs again

3. useRef

3-1. 정의

useRef는 두 가지 상황에 사용될 수 있다.

state과 같은 저장공간

기본적으로 React 함수형 컴포넌트는 재렌더링이 될 때 함수가 다시 호출돼 함수 내부의 값이 초기화된다. 그러나 useRef를 이용해 값을 선언하면 재렌더링이 되어도 언마운트 전까지 이전 값을 그대로 유지한다.

특정 DOM 요소에 접근할 때

3-2. 기본

3-2-1. 저장공간

useRef를 선언하면 {current:value} 객체를 반환한다.
ref.current로 접근해 원하는 값으로 바꿔줄 수 있다.

반환된 ref는 컴포넌트가 언마운트될 때까지 유지된다. 렌더링이 반복되더라도 언마운트만 되지 않으면

const ref = useRef(value)

3-2-2. DOM

document.querySelector 처럼 DOM에 접근해 조작할 수 있다.

const inputRef = useRef()

<input ref={inputRef} type="text" />

3-3. 활용

3-3-1. 저장공간

useState, useRef, 그냥 value를 선언했다.

state의 값을 증가시키는 버튼을 눌렀을 때 변경된 state의 값이 화면에 그대로 반영되어 나타난다. 값이 바뀔 때마다 재렌더링이 일어나기 때문이다.

반면 ref의 값을 증가시키는 버튼을 눌렀을 땐 변경된 ref의 값이 화면에서 바로 반영되어 나타나지 않는다. 내부적으로 ref의 값은 증가했지만, ref가 재렌더링을 일으키지 않기 때문에 화면에 반영되지 않는다.

여기서 state 버튼을 누르면 내부적으로 변경되었던 ref의 값이 업데이트 되어 화면에 나타난다.

value는 어떤 상황에든지 초기값인 1을 표시할 것이다. 화면이 재렌더링 될 때마다 함수 내부의 값은 초기화되기 때문이다.

import { useState, useRef } from 'react'

function App() {
  const [state, setState] = useState(1)
  const ref = useRef(1)
  let value = 1

  return (
    <div>
      <div>{state}</div>
      <div>{ref.current}</div>
      <div>{value}</div>
      <button onClick={() => setState(state + 1)}>state</button>
      <button onClick={() => (ref.current += 1)}>ref</button>
      <button onClick={() => (value += 1)}>value</button>
    </div>
  )
}

export default App

그럼 렌더링이 몇 번 일어났는지 화면에 표시해보자.

다음과 같이 함수를 작성하게 되면 엄청난 결과가 펼쳐진다.

import { useEffect, useState } from 'react'

function App() {
  const [count, setCount] = useState(1)
  const [rendering, setRendering] = useState(0)

  useEffect(() => {
    console.log('RENDERING')
    setRendering(rendering + 1)
  })

  return (
    <div>
      <div>{count}</div>

      <button onClick={() => setCount(count + 1)}>UP</button>
    </div>
  )
}

export default App

state 변경 => 재렌더링 => useEffect 호출 => state 변경 => 재렌더링 => useEffect 호출 ... 이렇게 무한 루프가 돈다.

다음과 같이 렌더링을 발생시키지 않는 useRef의 속성을 이용해 무한루프 없이 렌더링 횟수를 카운트해줄 수 있다.

import { useEffect, useState, useRef } from 'react'

function App() {
  const [count, setCount] = useState(1)
  const rendering = useRef(0)

  useEffect(() => {
    console.log('RENDERING')
    rendering.current += 1
  })

  return (
    <div>
      <div>{rendering.current}</div>

      <button onClick={() => setCount(count + 1)}>UP</button>
    </div>
  )
}

export default App

3-3-2. DOM

렌더링 될 때 자동으로 input 태그에 focus가 가도록 설정해보자.

import { useEffect, useRef } from 'react'

function App() {
  const inputRef = useRef()

  useEffect(() => {
    // 렌더링시 focus
    inputRef.current.focus()
  },[])

  return (
    <div>
      <input ref={inputRef} type="text" />
    </div>
  )
}

export default App

4. useContext + Context API

4-1. 정의

Context

애플리케이션 안에서 전역적으로 사용되는 데이터를 여러 컴포넌트가 공유할 수 있는 것

최상단 컴포넌트에서 최하단 컴포넌트까지 데이터를 전달하려면 중간에 위치한 컴포넌트를 거쳐야한다.
이를 props drilling이라고 한다.

props drilling

props를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트를 거치면서
컴포넌트 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정

  • 데이터는 하위 컴포넌트에서 실제 쓰이지만 모든 중간 컴포넌트를 거쳐야하는 불편함이 있다.
  • 개발 과정에서 데이터 전달이 잘못 되어버리면 모든 부모 컴포넌트를 타고 올라가 수정해야 한다.

리액트에서는 Context를 통해 props로 일일이 내려주지 않아도 상위 컴포넌트에서 선언한 값에 접근할 수 있다.
하위컴포넌트에서 해당 값에 접근할 때는 useContext 훅을 사용하면된다.

4-2. 기본

애플리케이션 전체 테마에 대한 데이터를 가지고 있는 컨텍스트를 하나 만들어보자.

//ThemeContext.js
import { createContext } from 'react'

export const ThemeContext = createContext(null)

그리고 Provider로 최상단 컴포넌트를 감싼다.
value에는 하위 컴포넌트에 전달해주고 싶은 값을 넣어준다.

// App.js
import { useState } from 'react'
import { ThemeContext } from './Page'

function App() {
  const [isDark, setIsDark] = useState(false)

  return (
    <ThemeContext.Provider value={{ isDark, setIsDark }}>
      <Page />
    </ThemeContext.Provider>
  )
}

export default App

그러면 useContext 훅을 통해 하위 컴포넌트에서 해당 값에 접근할 수 있다.

// Page.js
import React, { useContext } from 'react'
import { ThemeContext } from './ThemeContext'

export default function Page() {
  const data = useContext(ThemeContext)
  console.log(data) // {isDark:false, setIsDark:함수}

  return <div>Page</div>
}

4-3. 활용

그런데 만약 최상단 컴포넌트를 Provider로 감싸주지 않는다면 어떤 일이 일어날까?

우선 ThemeContext의 초기값으로 문자열을 지정한다.

// ThemeContext.js
import { createContext } from 'react'

export const ThemeContext = createContext('empty')

그리고 최상위 컴포넌트를 Provider로 감싸주지 않는다.

// App.js
import { useState } from 'react'

function App() {
  const [isDark, setIsDark] = useState(false)

  return (
      <Page />
  )
}

export default App

하위 컴포넌트에서 ThemeContext에 접근하면 초기값으로 설정해준 문자열 'empty'가 콘솔에 출력된다.

상위 컴포넌트에서 정해주는 value가 있나 없나 확인한 후, 없었기 때문에 바로 ThemeContext의 초기값을 불러온다.

// Page.js
import React, { useContext } from 'react'
import { ThemeContext } from './ThemeContext'

export default function Page() {
  const data = useContext(ThemeContext)
  console.log(data) // 'empty'

  return <div>Page</div>
}
profile
채워나가는 과정

0개의 댓글