Published on

리액트 공식문서 스터디 7-1 주차

Authors
  • avatar
    Name
    junyeol kim

Referencing Values with Refs

  • 컴포넌트가 렌더링에는 필요 없지만 내부적으로 사용해야 하는 값을 저장해야 할 때
  • 그리고 그 값이 바뀌어도 리렌더링을 일으키고 싶지 않을 때 ref를 사용한다.

Ref 사용방법

import { useRef } from 'react'

const ref = useRef(0)
  • useRef Hook을 React에서 import한 뒤, 컴포넌트 안에서 useRef(초기값) 형태로 호출해서 ref 객체 를 만든다.
  • useRef(0) 처럼 사용하면 { current: 0 } 형태의 객체가 반환되고, current 프로퍼티에 초기값이 들어 있다.

ref.current 특징

  • ref가 갖고있는 current 값은 읽고 쓸 수 있도록 intentionally mutable 하게 설계되어있다.
  • 또한, ref는 React가 추적하지 않는다.

예시코드를 통한 state와 ref 사용방식

  • 예시코드를 통해 state와 ref의 사용방식을 알아보자.

  • 사용자가 버튼을 클릭했을 때 시작과 정지를 하는 스톱워치를 만든다고 생각해보자.

  • 그렇다면, 첫 번째로 사용자가 Start 버튼을 클릭했을 때, 시간이 얼마나 지났는지 보여주게 하려면 Start 버튼을 누른 시각현재 시각 을 알고 있어야한다.

  • 이러한 정보들은 화면에 렌더링되어야하는 요소이므로 아래와 같이 state로 관리하게 된다.

const [startTime, setStartTime] = useState(null) // Start 버튼을 누른 시각
const [now, setNow] = useState(null) // 현재 시각
  • 그리고 Start 버튼을 클릭했을 때 이 state들을 세팅하고, 일정주기로 now를 업데이트하는 handler를 만든다.
function handleStart() {
  // Start를 누른 시각과 현재 시각을 모두 지금 시각으로 설정
  setStartTime(Date.now())
  setNow(Date.now())

  // 이후 10ms마다 now를 현재 시각으로 업데이트
  setInterval(() => {
    setNow(Date.now())
  }, 10)
}
  • 이 다음 Stop 버튼을 클릭했을 때 스톱워치를 멈추게 하려면 setInterval로 만든 타이머를 clearInterval로 취소해야한다.

  • clearInterval를 호출하려면 setInterval이 반환하는 interval ID가 필요하고 동시에 저장을 해야한다.

  • interval ID 값은 렌더링에 필요하지않고 오직 handler에서만 필요하기 때문에 여기서 ref가 필요하다.

import { useRef , useState } from "react";

...
const intervalRef = useRef(null); // setInterval이 반환한 interval ID를 저장

이후 handler를 아래와 같이 수정할 수 있다.

function handleStart() {
  setStartTime(Date.now())
  setNow(Date.now())

  // 기존 interval이 있다면 먼저 정리
  clearInterval(intervalRef.current)

  // 새 interval을 만들고, 반환된 ID를 ref에 저장
  intervalRef.current = setInterval(() => {
    setNow(Date.now())
  }, 10)
}

function handleStop() {
  // ref에 저장해 둔 ID로 interval 정지
  clearInterval(intervalRef.current)
}

state와 ref의 차이점

구분refstate
선언useRef(initialValue){ current: initialValue } 형태의 객체를 반환한다.useState(initialValue)는 현재 값과 setter 함수인 [value, setValue]를 반환한다.
값 변경 시 리렌더링current를 바꿔도 리렌더링이 일어나지 않는다.값을 업데이트하면 컴포넌트가 리렌더링된다.
가변성가변(mutable)이라 렌더링 과정 밖에서 current 값을 마음대로 읽고 쓸 수 있다.불변(immutable) 취급이라 반드시 setter 함수를 통해서만 변경을 요청한다.
읽기 시점렌더링 중에는 ref.current를 읽거나 쓰지 않는 것이 권장된다.언제든 읽을 수 있지만, 각 렌더는 그 시점의 state 스냅샷을 보는 식으로 동작한다.

useRef의 내부동작 방식

  • React 내부에서 useRef는 아래와 같이 구현되어 있다고 생각할 수 있다.
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue })
  return ref
}
  • 첫 렌더 때 { current: initialValue } 객체를 한 번 만들고, state에 저장한 뒤 이후 렌더에서도 동일한 객체를 그대로 반환한다.

  • state 업데이트 함수는 사용하지 않고, 컴포넌트 쪽에서 ref.current = ... 처럼 직접 필드를 바꿔 리렌더링 없이 값만 바뀌는 저장소 역할을 한다.

언제 refs를 사용해야할까?

  • 값을 저장해야할 때, 그 값이 바뀌어도 렌더링 로직이나 화면에 영향을 주지 않을때 ref를 사용한다.

refs를 잘 사용하는 방법

  • ref는 escape hatch처럼 사용해야한다.
    • 외부 시스템이나 브라우저 API(DOM, 타이머 등)와 상호작용할 때만 최소한으로 ref를 쓰고, 애플리케이션의 주요 로직과 데이터 흐름은 state/props에 두는 것이 좋다.
  • 렌더링 중에는 ref.current를 읽거나 쓰지 않는다.
    • 렌더링에 필요한 정보라면 ref가 아니라 state를 사용해야 한다. ref.current가 언제 바뀌는지 React는 모르기 때문에, 렌더 과정에서 이 값에 의존하면 컴포넌트 동작을 예측하기 어려워진다.
  • state에 적용되는 제약은 ref에는 없다.
    • state는 렌더마다 스냅샷처럼 동작하고, 업데이트도 비동기 큐에 쌓이지만, ref는 그냥 JS 객체라 ref.current = 5; console.log(ref.current); 하면 즉시 5가 찍힌다. 또한 ref는 “불변성 유지”를 신경 쓸 필요가 없다. 단, 그 객체가 렌더링(화면) 계산에 사용되지 않을 때에만 마음대로 mutate해도 된다.

ref와 DOM

  • ref는 어떤 값이든 가리킬 수 있지만, 가장 흔한 사용처는 DOM 요소 를 가리키는 것이다.

  • JSX에서 <div ref={myRef}> 처럼 ref 속성에 ref를 넘기면, React가 해당 DOM 요소를 찾아서 myRef.current 에 넣어 준다.

  • 나중에 그 요소가 DOM에서 제거되면, React가 myRef.current를 자동으로 null로 바꿔 준다.


Manipulating the DOM with Refs

  • React는 렌더링 결과물에 맞춰 DOM 변경을 자동으로 하기 때문에 컴포넌트에서 DOM을 조작할 필요가 많이 없다.

  • 하지만 특정 노드에 포커스를 옮기거나, 스크롤 위치를 옮기거나, 위치와 크기를 측정 하기 위해 React가 관리하는 DOM 요소에 접근해야 할 때가 있다.

  • 이러한 DOM 에 접근하기 위해 필요한 방법이 이전 챕터에서 배운 Ref 이다.

Ref로 Node 가져오기

  • 아래와 같이 useRef로 ref 객체를 만들고 컴포넌트의 ref 속성에 연결하면, 해당 DOM Node가 렌더 후 ref.currnet에 들어가서 브라우저 API 등을 직접 호출할 수 있다.
import { useRef } from 'react'

const myRef = useRef(null)

;<div ref={myRef}>...</div> // <div>의 DOM Node를 myRef.current에 넣어달라는 의미

myRef.current.scrollIntoView() // 브라우저 API를 직접 호출할 수 있다

Ref Callback으로 Ref list 관리하기

  • Hook 규칙으로 useRef는 map안에서 사용하지 못하는데, 이러한 동적으로 변하는 리스트의 DOM들을 한 번에 관리하기 위해서
    ref callback + Map 을 사용한다.

다른 컴포넌트의 DOM nodes 접근하는 방법

  • 커스텀 컴포넌트에 ref를 직접 달아도 곧바로 DOM을 가리키지 않고, 기본적으로 null이거나 컴포넌트 인스턴스를 가리킨다.

  • 부모에서 만든 ref자식에게 props처럼 내려보낸 뒤, 자식에서 그 ref를 실제 내장 컴포넌트(<input>, <div> 등)의 ref에 다시 전달해야 최종적으로 부모의 ref.current가 해당 DOM 노드를 가리키게 된다.​

When React attaches the refs

  • React의 업데이트 과정은 총 2단계이다.

    • Render : 컴포넌트를 호출하여 화면에 무엇을 나타낼지 계산한다.
    • Commit : 변경사항을 DOM에 적용한다.
  • Render 단계에서는 DOM이 아직 없거나 업데이트 전이라 ref.current가 null이거나 오래된 값일 수 있으므로, 이 시점에는 ref를 읽지 않는 것이 좋다.

  • Commit 단계에서 React는 DOM을 바꾸기 직전에 관련된 ref.current들을 null로 초기화한 뒤 DOM을 실제로 변경하고, 곧바로 새 DOM 노드들로 각 ref.current를 다시 채운다.

  • 그래서 ref를 이용한 DOM 접근·조작은 보통 클릭 같은 이벤트 핸들러나, 특정 이벤트 없이 커밋 이후에 실행되는 Effect 안에서 수행해야 안전하다.​

Flushing state updates synchronously with flushSync

  • flushSync는 특정 state 업데이트를 바로 DOM까지 반영한 뒤, 그 다음 코드를 실행하고 싶을 때 그 업데이트만 동기적으로 flush 해 주는 함수다.

Best practices for DOM manipulation with refs

  • focus, scroll 등 브라우저 API 호출처럼 렌더링에 영향이 없는 것은 Ref로 한다.

  • node를 제거하거나, child를 추가, 삭제하는 등 렌더링에 영향을 주는 것은 컴포넌트 + state로 하는 것이 좋다.


Synchronizing with Effects

What are Effects and how are they different from events?

  • 렌더링 이후 ReactDOM 업데이트를 끝낸 후 실행되는 코드를 Effect라고 한다.
  • Rendering code순수 함수처럼 DOM과 무관하게 계산만 수행해야 한다.
  • Event handler사용자의 명시적 인터랙션 ( 클릭, 입력 등 ) 으로 발생하지만, Effect는 렌더링 결과에 의해 자동으로 발생하는 side effect이다.

You might not need an Effect

  • Effect는 렌더링 결과를 외부 시스템( 브라우저 API, 서드 파티 위젯, 네트워크 )과 동기화할 필요가 있을 때만 필요
  • Effect가 단순히 다른 상태에 기반하여 일부 상태를 조정하는 경우에는 Effect가 필요하지 않을 수 있다.

How to write an Effect

Step 1 : Declare an Effect

import { useEffect } from 'react'

function MyComponent() {
  useEffect(() => {
    // 렌더링 이후 실행할 코드
  })
  return <div />
}
  • 기본적으로 모든 렌더링 이후에 실행된다. (의존성 배열이 없을 경우)
  • DOM/외부 API 접근같은 side effect를 작성할 때 사용한다.

Step 2 : Specify the Effect dependencies

useEffect(() => {
  // effect body
}, [dep1, dep2])
의존성 배열실행 시점
없음모든 렌더링 후 실행
[] (빈 배열)마운트 시 1회 실행
[dep1, dep2]dep가 변경될 때만 실행
  • 의존성 배열을 통해 불필요한 실행을 막을 수 있다.
  • 의존성 누락은 lint 오류가 발생하고, React가 해당 오류에 대한 수정사항을 자동으로 제안한다.

Step 3 : Add cleanup if needed

useEffect(() => {
  const conn = createConnection()
  conn.connect()
  return () => {
    conn.disconnect()
  }
}, [])
  • Subscription, Connection, Timer 같은 경우는 cleanup과 같은 정리코드가 필요하다.

  • cleanup 같은 경우 Effect가 다시 실행되기 전 또는 컴포넌트 언마운트시에 실행된다.

How to handle the Effect firing twice in development?

  • React는 Strict Mode가 활성화된 개발 환경에서, effect 관련 버그를 조기에 발견하기 위해 컴포넌트를 의도적으로 다시 마운트하며, 그 결과 Effect가 두 번 실행되는 것처럼 보인다.

  • 이러한 경우의 일반적인 해결책은 cleanup 함수를 구현하는 것이다.

  • Effect가 두 번 실행되는 것을 막기 위해 useRef로 실행을 제한하는 것은 버그를 숨길 뿐 해결하지 못하므로 피해야 한다.

Controlling non-React widgets

useEffect(() => {
  const map = mapRef.current
  map.setZoomLevel(zoomLevel)
}, [zoomLevel])
  • React로 작성되지 않은 외부 UI 위젯(지도, 차트 등)을 제어할 때도 Effect를 사용한다.
  • 동일한 값을 여러 번 설정해도 문제가 없는 API라면 cleanup이 필요 없다.
    • 개발 모드에서 Effect가 두 번 실행되어도 setZoomLevel을 두 번 호출하는 것은 안전하다.
  • 반대로, 연속 호출을 허용하지 않는 API라면 cleanup이 필요하다.

Subscribing to events

useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY)
  }
  window.addEventListener('scroll', handleScroll)
  return () => window.removeEventListener('scroll', handleScroll)
}, [])
  • Effect가 어떤 것을 subscription 한다면, cleanup 함수에서 반드시 unsubscribe 해야 한다.

Triggering animations

useEffect(() => {
  const node = ref.current
  node.style.opacity = 1
  return () => {
    node.style.opacity = 0
  }
}, [])
  • Effect가 요소의 애니메이션을 트리거한다면, cleanup 함수에서 초기 상태로 되돌려야 한다.

Fetching data

useEffect(() => {
  let ignore = false

  async function startFetching() {
    const json = await fetchTodos(userId)
    if (!ignore) {
      setTodos(json)
    }
  }

  startFetching()

  return () => {
    ignore = true
  }
}, [userId])
  • Effect가 데이터를 가져온다면, cleanup에서는 fetch를 중단(abort) 하거나 결과를 무시(ignore) 해야 한다.

  • 이미 시작된 네트워크 요청은 취소할 수 없지만, 더 이상 관련 없는 응답이 state를 변경하지 않도록 보장해야 한다.

  • 개발 모드에서는 네트워크 요청이 두 번 보일 수 있으나, cleanup 덕분에 첫 번째 요청 결과는 무시된다.

Sending analytics

useEffect(() => {
  logVisit(url)
}, [url])
  • 개발 환경에서는 분석 이벤트가 두 번 전송될 수 있다.

  • 이는 문제가 아니며, 개발 환경에서는 로그가 실제 지표에 영향을 주지 않아야 한다.

  • 빌드 환경에서는 중복 로그가 발생하지 않는다.

  • 분석 디버깅이 필요하다면

    • 빌드 모드에서 실행하거나

    • Strict Mode를 일시적으로 끌 수 있다.

  • 또는 Effect 대신 라우트 변경 이벤트에서 분석을 보낼 수도 있다.

Not an Effect: Initializing the application

if (typeof window !== 'undefined') {
  checkAuthToken()
  loadDataFromLocalStorage()
}
  • 애플리케이션 시작 시 단 한 번만 실행되어야 하는 로직은 Effect에 넣지 않는다.

  • 컴포넌트 외부에 두면, 브라우저 로드 시 한 번만 실행됨이 보장된다.

Not an Effect: Buying a product

// 잘못된 예
useEffect(() => {
  fetch('/api/buy', { method: 'POST' })
}, [])
  • 제품 구매처럼 사용자 행동에 의해 발생해야 하는 로직은 Effect에 두면 안 된다.

  • Effect는 재마운트 시 다시 실행되므로, 사용자가 의도하지 않게 중복 구매가 발생할 수 있다.

  • 이런 로직은 아래와 같이 이벤트 핸들러로 옮겨야 한다.

function handleClick() {
  fetch('/api/buy', { method: 'POST' })
}