개발새발 로그

ref를 사용하면서 "왜?" 라는 생각 안 드세요? 본문

React

ref를 사용하면서 "왜?" 라는 생각 안 드세요?

이즈흐 2024. 4. 11. 04:53

ref를 사용하고, 학습하면서 생각하게된 궁금점이 많았다.

궁금점에 대한 해답을 찾으면서 학습한 내용을 정리해봤다.

 

🤔왜 ref를 props로 전달하면 안될까?

useRef를 사용해서 자식 컴포넌트에 ref를 전달할 때 이렇게 코드를 작성할 수 있다.

export default function MainPage() {
  const ref = useRef(null)

  return (
    <section>
      <input ref={ref}></input>
      <ChildComponent ref={ref} />
    </section>
  )
}

const ChildComponent = ({ ref }) => {
  useEffect(() => {
    console.log(ref)
  }, [ref])

  return <div>하이</div>
}

그러면 아래와 같이 경고문을 띄우게 된다.

또한 undefined를 반환한다.

이 경고 메시지는 React에서 자식 컴포넌트에 ref를 prop으로 전달하려고 시도했을 때 나타난다.

 

🔴Warning: ChildComponent:ref is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

자식 컴포넌트: ref는 소품이 아닙니다. 접근을 시도하면 정의되지 않은 값이 반환됩니다. 자식 컴포넌트 내에서 동일한 값에 액세스해야 하는 경우 다른 프로퍼티로 전달해야 합니다.

 

React에서 ref는 컴포넌트에 대한 참조를 생성하기 위해 사용되는 특별한 속성이다.

즉 모든 React 컴포넌트는 기본적으로 ref props를 가지고 있는데(예약어), 그것과 겹치게 되므로 다른 방식으로 사용하라고 에러를 띄어 주는 것이다.

일반 prop과 같은 방식으로 자식 컴포넌트에 전달할 수 없는 것이다.

 

🔴Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

경고: 함수 컴포넌트에는 참조를 지정할 수 없습니다. 이 참조에 접근하려는 시도는 실패합니다. React.forwardRef()를 사용하려고 하셨나요?

그리고 리액트 공식문서에서는 이미지에서 두 번째 오류에 대해 이렇게 말하고 있다. 

기본적으로 React가 컴포넌트가 다른 컴포넌트의 DOM 노드에 접근하는 것을 허용하지 않기 때문에 발생합니다. 심지어 자신의 자식에게도요!
이것은 의도적인 것입니다.
Ref는 드물게 사용해야 하는 탈출구입니다.
다른 컴포넌트의 DOM 노드를 수동으로 조작하면 코드가 훨씬 더 취약해집니다.

대신, 자신의 DOM 노드를 노출하고자 하는 컴포넌트는 그러한 행동을 선택해야 합니다.
컴포넌트는 자신의 ref를 자식 중 하나에게 "전달"한다는 것을 명시할 수 있습니다.
여기 MyInput이 forwardRef API를 사용하는 방법이 있습니다: (...)

디자인 시스템에서는 버튼, 입력 필드 등과 같은 저수준(low-level) 컴포넌트가 자신의 DOM 노드로 ref를 전달(forward)하는 것이 일반적인 패턴입니다.
이와 반대로, 폼, 리스트, 페이지 섹션과 같은 고수준(high-level) 컴포넌트는 대부분 DOM 구조에 대한 우발적인 의존성을 피하기 위해 자신의 DOM 노드를 노출시키지 않습니다

 

 

💭왜 코드가 취약해지는 것일까?

리액트에서 ref를 사용하는 주된 목적은 리액트 컴포넌트가 생성하는 DOM 노드나 리액트 요소에 "직접적으로" 접근하기 위함이다.

이러한 접근은 주로 DOM을 조작하거나, 특정 DOM 이벤트를 처리하거나, 입력 필드에 포커스를 맞추는 등의 작업을 수행할 때 필요하다.

그러나 리액트는 선언적인 방식으로 UI를 구성하는 것을 지향하기 때문에, ref의 사용은 가능한 한 제한되어야 한다.

 

 

(그럼 ref 사용을 지양해야하는 건가 해서 추가로 찾아봤는데 " React에서 컴포넌트 간의 DOM 노드 접근을 제한하는 것은 의도적인 설계입니다. React의 선언적 UI 패러다임을 유지하고, 데이터 흐름을 단방향으로 유지하기 위함입니다. 즉, UI는 항상 그 상태(state)를 반영해야 하며, 상태 변경은 React의 데이터 흐름에 의해서만 관리되어야 합니다." 라고 한다.)

(공식문서에서도 "선언적으로 해결될 수 있는 문제에서는 ref 사용을 지양해야한다"라고 했다고 한다.)

컴포넌트 간의 ref 전달

리액트는 컴포넌트가 자식 컴포넌트의 DOM 노드에 직접 접근하는 것을 기본적으로 허용하지 않는다.

이는 컴포넌트의 캡슐화를 유지하고, 컴포넌트 간의 결합도를 낮추기 위함이다.

즉, 각 컴포넌트는 가능한 한 독립적으로 유지되어야 하며, 외부 컴포넌트가 내부 구현에 의존하지 않도록 해야 한다.

1. 컴포넌트 간의 강한 결합: ref를 직접 전달하게 되면, 부모 컴포넌트가 자식 컴포넌트의 내부 DOM 노드나 인스턴스에 직접 접근할 수 있게 됩니다.
이는 자식 컴포넌트의 구현 세부 사항에 부모 컴포넌트가 의존하게 만들어, 두 컴포넌트 간의 결합도를 높이며 재사용성과 유지 보수성을 저하시킵니다.

2. 컴포넌트의 캡슐화 위배: 컴포넌트는 가능한 한 독립적이고 캡슐화되어야 합니다.
ref를 직접 전달하면, 자식 컴포넌트의 내부 구조가 외부로 노출되어 캡슐화가 약화됩니다.
이는 컴포넌트의 내부 구현 변경 시 외부 컴포넌트에도 영향을 줄 수 있으며, 장기적으로 코드의 유지 보수성을 해칠 수 있습니다.

3. React의 추상화 원칙 위배:
 React는 컴포넌트 간의 명확한 인터페이스를 통해 데이터와 로직을 공유하는 것을 목표로 합니다.

ref는 컴포넌트의 내부 상태나 메서드에 접근하기 위한 수단이 아니라, DOM 접근이나 인스턴스 참조를 위한 특별한 목적을 가지고 있습니다.
ref를 일반 props처럼 사용하는 것은 이러한 설계 원칙을 어기는 행위입니다.

 

💭저수준 컴포넌트에서 ref 사용은 괜찮지만 고수준 컴포넌트에서의 ref 사용이 문제가 되는 이유?

저수준 컴포넌트에서의 ref 사용

저수준 컴포넌트는 보통 UI의 가장 기본적인 단위를 나타낸다.

예를 들어, <input>, <button>, <div> 등과 같은 HTML 엘리먼트를 직접 래핑하는 컴포넌트들이다.

이러한 컴포넌트들에서 ref를 사용하는 것은 괜찮다.

왜냐하면, 이들은 주로 DOM에 직접적인 접근이 필요한 작업을 수행하기 때문이다.

예를 들어, 입력 컨트롤에 포커스를 주거나, DOM 요소의 크기를 직접 측정하는 것과 같은 작업들이 이에 해당한다.

이런 경우, ref는 필수적이며 유용한 도구다.

고수준 컴포넌트에서의 ref 사용

고수준 컴포넌트는 여러 저수준 컴포넌트 혹은 다른 고수준 컴포넌트들을 조합하여 만들어진, 보다 복잡한 기능을 제공하는 컴포넌트다.

이들은 특정 기능을 추상화하고, 애플리케이션의 더 높은 수준의 로직을 처리한다.

고수준 컴포넌트에서 ref 사용이 문제가 되는 주된 이유는, 이러한 컴포넌트들이 제공하는 추상화 수준 때문이다.

고수준 컴포넌트의 내부 구현은 외부에서 알 필요가 없어야 하며, 이를 통해 내부 구현을 자유롭게 변경할 수 있다.

만약 고수준 컴포넌트의 내부 DOM 요소에 직접 접근하기 위해 ref를 사용한다면, 이는 컴포넌트의 추상화 경계를 침범하는 것이 된다.

이로 인해 컴포넌트의 재사용성 유지 보수성이 저하될 수 있다.

또한, 고수준 컴포넌트의 경우, 내부 상태나 로직에 의존하는 복잡한 동작을 수행할 가능성이 높다.

이런 상황에서 ref를 사용하여 내부 요소에 직접 접근하려고 하면, 예측하지 못한 부작용이 발생할 수 있으며, 이는 애플리케이션의 버그로 이어질 수 있다.

 

💭그럼 ref를 다른 이름으로 넘기면 되는거 아니야?

그럼 어찌됐든 ref는 예약어니까 다른 이름으로 넘기면 되지 않을까? 라는 생각을 하게 된다.

아래처럼 말이다.

export default function MainPage() {
  const ref = useRef(null)

  return (
    <section>
      <input ref={ref}></input>
      <ChildComponent myRef={ref} />
    </section>
  )
}

const ChildComponent = ({ myRef }) => {
  useEffect(() => {
    console.log(myRef)
  }, [myRef])

  return <div>하이</div>
}

 

이 코드를 실행하면 아래와 같이 정상적으로 출력된다.

그리고 이는 클래스형 컴포넌트와 함수형 컴포넌트 동일하게 작동한다.

 

그럼 이렇게 사용하면 되는거 아니야? 라고 생각하게 된다.

 

📖forwardRef의 탄생 배경

하지만 위와 같은 방식이 좋은 방법이 아니다.
같은 역할을 하고는 있지만, 결국에는 DOM 엘리먼트에 접근하기 위해서는 ref가 아닌 다른 이름으로 계속해서 전달해야하고,
이러한 전달은 일관적으로 보이지 않을 수 있다.

 

forwardRef가 탄생한 배경이 이것이다.

ref를 전달하는 데 있어서 일관성을 제공하기 위해서다.

 

어떤 props명으로 전달할지 모르고, 이에 대한 완전한 네이밍의 자유가 주어진 props 보다는 fowardRef를 사용하면 좀 더 확실하게 ref를 전달할 것임을 예측할 수 있고, 또 사용하는 쪽에서도 확실히 안정적으로 받아서 사용할 수 있다.

 

사용방법

export default function MainPage() {
  const ref = useRef()

  return (
    <section>
      <ChildComponent ref={ref} />
    </section>
  )
}

const ChildComponent = forwardRef((props,ref) => {
  useEffect(() => {
    console.log(ref)
  }, [ref])

  return <div>하이</div>
})

먼저 받고자하는 컴포넌트를 forwardRef로 감싸고, 두 번째 인수로 ref를 전달받는다.

그리고 부모 컴포넌트에서는 동일하게 props.ref를 통해 ref를 넘겨주면 된다.

 

이렇게 forwardRef를 사용하는 코드로 수정하면 ref를 props로 전달할 수 있고, 전달 받은 컴포넌트에서도 ref라는 이름을 그대로 사용할 수 있다.

 

 

 


 

 

🤔그럼 왜 ref를 사용해야할까?

위 질문에 대해서 궁금증이 생겼다.

 

📖ref말고 자바스크립트에서 사용하던 querySelector와 같은 API를 사용하면 안되는걸까?

 

React에서 document.querySelector를 사용하게되면, 실제 DOM의 요소를 가져오게 된다.

하지만 React는 Virtual DOM을 통해 Real DOM을 그리기 때문에, React가 제어하고있는 Virtual DOM 안에 있는 요소가 더 신뢰할만하다. 

=> DOM API로 Real DOM에 있는 요소를 가져왔는데, 이 요소가 현재 Virtual DOM을 통해 Real DOM에 존재하는 요소인지 확신할 수 없다는 것 때문이라고 한다.

 

📖 그럼 querySelector와 같은 API를 사용하면 무슨 문제가 있을까?

state 문제

React를 사용하게되면, 가장 중요한 개념 중 하나가 바로바로 State이다. React 내부에서 데이터는 컴포넌트 내의 State으로 조작된다.  즉, React가 State를 컨트롤(제어)하고 있다. 만약 이러한 React 시스템을 벗어나 DOM을 직접적으로 건드리게되면 이 내용들은 React가 제어하는 영역에서 벗어나게 되고, 이렇게 React의 제어를 벗어나게 되면, React에서 제공하는 이점들을 사용할 수 없게 된다. 

 

디버깅이 어려워짐

또 이렇게 React가 제어하는 State와 제어하지 않는 State을 혼용해서 사용해 데이터를 조작할 경우, 위에 언급했듯이 React의 Lifecycle에 맞추어 DOM Element를 가져오지 못해 가져온 DOM Element를 신뢰할 수 없어지는 문제가 발생한다. 이렇게 데이터를 어디에서 어떻게 조작하고 있는지 예측하기 어렵기 때문에 디버깅 또한 어려워진다. 

그렇기 때문에 React에서 DOM Selector를 사용하고싶으면 React의 Lifecycle과 함께 동작하는 ref를 사용하는 것이 더 바람직하다.

 

closest, querySelector 등을 사용하게되면, 결국 구조에 종속적인 코드를 짜게 되는 문제

React는 선언형 프로그래밍을 권장 하고 있는데 querySelector와 같은 API를 사용하게 되면 명령형 프로그래밍을 하게 된다.

우리가 querySelector, closest로 작성한다면 어떻게 될까?

어떤 한 요소를 콕 찝어 가져와 '이 요소에 이 이벤트를 붙여줘!' '이 요소에 이런 속성을 부여해줘!' 라고 말하는 것은 명령형에 더 가까워보인다. 어떤 컴포넌트를 만들면서 이렇게 구조에 종속적인 코드를 넣게 되면, 이 코드는 결국 재사용하기 힘든, 명령형 컴포넌트가 될 것이다. 

📖 ref는 정말 필요로 할 때가 아닌 이상은 ref를 사용하는 것을 지양하도록 권고?

(공식 문서)

왜 그러는 걸까?

그 이유는 DOM을 조작하는 이벤트 자체가 우선 무겁다는 점이다. 실제 DOM에 접근하려면 DOM 자체의 데이터를 받아와야하기 때문에 거기서부터 오는 정보의 무게감이 들 수 밖에 없다.

React는 최대한 그런 DOM을 건드리지 않기 위해 가상 DOM을 활용하고 있다.

그러다보니 ref는 그런 실제 DOM의 정보를 참고하기 위해 쓰이는 용도이므로 되도록이면 필요할 때 빼고는 지양하는 게 좋다고 한다.

 

 


 

 

🤔왜 ref의 current로 접근해야하는 걸까?

querySelector처럼 그냥 DOM 요소를 반환해주면 좋을 텐데,
왜 createRef/useRef는 왜 객체를 반환하고 current 프로퍼티로 DOM 요소를 전달해주는 것일까?

이는 React가 가상 돔을 기반으로 작동하는 라이브러리라는 사실을 생각해보면 이유가 명확해진다.


아래 공식문서 문장을 보자

공식문서 

컴포넌트가 마운트될 때 React는 current 프로퍼티에 DOM 엘리먼트를 대입하고, 컴포넌트의 마운트가 해제될 때 current 프로퍼티를 다시 null로 돌려놓습니다.
ref를 수정하는 작업은 componentDidMount 또는 componentDidUpdate 생명주기 메서드가 호출되기 전에 이루어집니다.

실제 DOM에 React 노드가 렌더될 때까지 ref가 가리키는 DOM 요소의 주소 값은 확정된 것이 아니다.

 

즉 우리가 ref에 접근할 수 있는 시점은 React 노드가 실제로 DOM에 반영되는 시점부터다.
(라이프사이클상에서는 componentDidMount) 그 이전에는 null이 current 프로퍼티에 담긴다. (useref는 초깃값을 따로 인자로 전달해줄 수 있다.)

 

그리고 가상 DOM이 변경될 때 실제 DOM의 요소도 변경되는 경우가 있기 때문에 DOM이 업데이트되는 경우(componentDidUpdate)도 ref의 current 값이 변경되게 된다.


이처럼 유동적이기에 React는 객체를 반환해 current 프로퍼티의 값을 계속해서 수정한다.

 

쉽게 정리하자면 아래와 같다.

 

React에서 ref를 사용할 때 current라는 키를 통해 접근하는 이유는 ref가 변경 가능한 값을 유지하기 위해 사용되는 객체임을 반영하기 위해서다.

 

React의 ref는 컴포넌트의 생명주기 동안 변할 수 있는 값, 특히 DOM 요소나 컴포넌트 인스턴스에 대한 참조를 저장하는 데 사용됩니다. 이러한 특성 때문에 React는 ref를 단순한 참조가 아닌, 변경 가능한 현재 값(current 값)을 저장할 수 있는 객체로 설계한 것이다.

 

📖객체인 것이 무엇이길래?

공식문서에서는 useRef를 아래와 같이 표현하고 있습니다.

ref는 숫자를 가리키지만, state처럼 문자열, 객체, 심지어 함수 등 모든 것을 가리킬 수 있습니다.
state와 달리 ref는 읽고 수정할 수 있는 current 프로퍼티를 가진 일반 자바스크립트 객체입니다.
컴포넌트는 모든 증가에 대하여 다시 렌더링 되지 않습니다. state와 마찬가지로 ref도 React에 리렌더에 의해 유지됩니다.
그러나, state를 설정하면 컴포넌트가 다시 렌더링 됩니다. ref를 변경하면 다시 렌더링 되지 않습니다!

 

객체인 것이 무슨 상관인지 알려면 Heap에 대해서 알아야한다.

 

자바스크립트 엔진은 Memory Heap 과 Call Stack 으로 구성되어 있습니다.

 

콜스택(Call Stack)에는 원시타입 데이터가 저장되고,

실행 콘텍스트(Execution Context)를 통해

  1. 변수 식별자(이름) 저장
  2. 스코프 체인 및 this 관리
  3. 코드 실행 순서 관리

등을 수행한다.

 

메모리 힙(Memory Heap)에는 참조 타입(객체 등) 데이터가 저장된다.
메모리 할당이 일어나는 곳이다.

 

여기서 중요한 것이 heap이다.

heap 은 전역변수와 참조타입의 변수를 할당하고 가비지 컬렉터를 이용해 사용하지 않는(=참조되지 않는) 메모리를 해제시킨다.

즉 우리가 자바스크립트 객체로 만드는 변수들은 모두 heap 공간에 할당되었다가 해제된다.

 

그럼 다시 useRef로 돌아와서

1. useRef() 는 일반적인 자바스크립트 객체다.

즉 heap 영역에 저장되는 변수다.

2.매번 렌더링할 때 동일한 객체를 제공한다.

heap에 저장되어 있기 때문에 어플리케이션이 종료되거나 가비지 컬렉팅될 때 까지, 참조할때마다 같은 메모리 값을 가진다고 할 수 있다.

3.값이 변경되어도 리렌더링이 되지 않는다.

같은 메모리 주소를 갖고있기 때문에 자바스크립트의 === 연산이 항상 true 를 반환합니다. 즉 변경사항을 감지할 수 없어서 리렌더링을 하지 않는다는 뜻이다.

 

 

🤔useRef는 어떻게 구성되어있는 걸까?(TMI 많음)

공식문서에서는 아래와 같이 설명하고 있다.

"useState로 구현된다고 상상할 수 있다"라고 하고있다.

 

그런데 React Deep Dive 책에서는 아래와 같이 말하고 있습니다.

useReft는 어떻게 구현돼 있을까?
리액트에서의 구현은 다르지만 Preact에서 구현에 대한 힌트를 얻을 수 있다.
// Preact에서의 useRef 구현
export function useRef(initialValue){
	currentHook = 5
    return useMemo(()=>({current:initialValue}),[])
}
값이 변경돼도 렌더링되면 안 된다는 점, 실제 값은 {current: value}와 같은 객체 형태로 있다는 점을 떠올려보자
렌더링에 영향을 미치면 안 되기 때문에 .useMemo에 의도적으로 빈 배열을 선언해뒀고, 이는 각 렌더링 마다 동일한 객체를 가리키는 결과를 낳을 것이다.
자바스크립트의 특징, 객체의 값을 변경해도 객체를 가리키는 주소가 변경되지 않는다는 것을 떠올리면 useMemo로 useRef를 구현할 수 있다.

 

useMemo로 구현되어있다고 얘기하고 있다.

 

📖그럼 실제 리액트 내부에서는 어떻게 구현되어 있을까?

react코어는 react Element에 대한 정보만 알고 있고,

react element는 fiber로 확장해야 hook을 포함한다.

그 확장은 reconciler가 하게 된다.

즉 reconciler가 hook을 알고 있다.

 

react 코어는 hook을 사용하기 위해 외부에서 주입받는다(의존성을 끊기 위해 => 이곳저곳에서 사용가능하도록)

hook의 출처는 아래와 같다.

reconiler -> shared/ReactSharedInternal -> react/ReactSharedInternal -> react/ReactCurrentDispathcer 
-> react/ReactHooks -> react -> 개발자

 

 

그럼 reconciler에서 어떻게 hook을 할당할까?

이를 위해서는 리액트 코드를 까봐야한다.

이 과정은 복잡하며 아래 설명은 간단하게 설명하려고 한다.

 

react/packages/react-reconciler/src/ReactFiberHook.js에서 renderWithHooks라는 함수가 hook과 함께 Render, 즉 hook을 주입하는 역할을 한다.

(Render : 컴포넌트 호출 후 그 결과가 VDOM에 반영되는 과정)

 

ReactFiberHook.js에서 아래로 가면 아래와 같이 react에서 최종적으로 Hook을 사용할 때 접근하는 데이터ReactCurrentDispatcher에 무엇을 할당하고 있다.

nextCurrentHook === null이 무엇을 의미할까?

위로 올라가면 위처럼 초기화 하고 있다.

여기서 Point는 아래와 같다.

1. 현재 변수들이 let과 같은 키워드로 선언되어있지 않다. 즉 전역변수로 선언되어있다.

2. workInProgress는 현재 렌더딩 하려는 Fiber다.

3. current현재 DOM에 반영되고있는 Fiber다. 즉 현재 반영되어있는 fiber가 비어있지 않다는 것은 mount가 되어있다는 뜻이다. 그래서 current의 memoizedState라는 값을 넘기고 있다.

 

그래서 아까 봤던 위 if문은 mount할 때와 update할 때를 구분해서 할당하는 것이다.

그럼 아래와 같은 데이터를 할당한다.

이름에서도 보이듯이 mount와 update로 구분되어있는 모습이다.

 

 

이후 Component(props,refOrContext)를 호출하는데 이는 컴포넌트를 호출하는 것이다.

렌더링 과정에서 컴포넌트 호출을 하고 있는 것이다.

 

 

컴포넌트를 호출하고 또 아래에서 ReactCurrentDispatcher에 무엇을 할당하고 있다.

그럼 덮어씌워지는 것인가 생각할 수 있다.

하지만 아까 컴포넌트 호출에서 정말 많은 일들이 일어난다.

그 일이 다 일어나고, 더 이상 hook을 호출할 수 없을 때 (가 에러를 발생하는 상황이나 unMount 되는 상황)

그럴 때는 hook을 호출해서는 안된다.

그럴 때를 대비해 에러를 발생시키기 위 hook을 저장해야하는 ReactCurrentDispatherContextOnlyDispatcher을 할당하고 있는 것이다.

 

그럼 ContextOnlyDispatcher가 뭔데?

throwInvalidHookError라는 것을 할당하고 있다.

이는 "Hook을 호출하면 안되는 곳에서 호출할 때 에러를 던지는 것"이다.

hook을 호출했을 때 에러를 던지기 위한 것들을 hook에다가 덮어 씌우는 것이다.

 

그리고 아래와 같이 workInPorgress를 가져와서 아래와 같이 할당하고 있다.

memoizedState에 무엇을 할당하는 것을 볼 수 있다.

firstWorkInProgressHook이라는 것을 할당하고 있는데

 

아래와 같이 정말 Hook을 넣고 있다.

hook이 객체로 구성되어있고 5가지의 key를 가지고 있다.

이에 대한 자세한 역할은 추후 포스팅에서 필요하다면 자세히 설명하기로 하고,

 

간단하게 설명하자면

1. hook.memoizedState는 마지막에 얻은 state값

2. hook.next는 다음 hook을 가리키는 pointer(hook은 링크드 리스트에 저장)

3. hook.queue는 hook을 호출할 때 마다 update 객체를 링크드 리스트로 구현한 queue 에 저장(setState가 한번에 수행되는 이유)

4. baseState와 baseUpdate는 추후 학습 후 포스팅 예정..

그럼 이 mountWorkInProgressHook은 언제 호출하는 것일까?

아래 코드에서 보이듯이 mountState에서 호출하고있다.

mountState는 아까 여기서 본 것이다.

 

 

정리하자면 렌더링 중인 Fiber에 hook정보를 연결하고 있는 것이다.

그것을 reconciler가 하고 있는 것이다!

 

추가로 설명할 내용이라면

아래 처럼 hook을 연결하고 초기화하는 부분이있다.

하나같이 전역 변수임을 알 수 있다.

그러면 파일내 다른 곳에서 계속 쓰인다는 것인데 이를 초기화해서 다시 사용하는 것이다.

 

이는 컴포넌트가 호출되고, 작업한 다음

다음 컴포넌트를 호출하고 작업하기 위해서 초기화를 하는 것이다.

왜냐하면 이전 컴포넌트의 작업내용이 담기면 안되기 때문이다.

 

그리고 renderWithHooks()는 children을 반환하면서 끝이난다.

이 children은 컴포넌트를 호출해서 반환된 값을 담아둔 변수였다.

 

이렇게 hook이 어떻게 관리되는지 알아봤다.

 

그럼 이제 아까 처음에 궁금했던 useRef가 어떻게 관리되는지 확인할 수 있다.

이런 식으로 reconciler에서 mountRef를 구성하고 있다.

아까  말한 것처럼 mountWorkInProgressHook()을 호출해서 가져오고 

ref라는 일반적인 자바스크립트 객체를 만든 다음 hookmemoizedStateref를 넣는 모습이다.

 

그러니까 ref는 정말 일반적인 자바스크립트 객체인 것을 확인할 수 있다! 

하지만 이 정보는 개인적으로 찾은 것이므로 확실하지 않을 수 있습니다

 

 

🤔ref를 하위컴포넌트에 props로 내려주고, ref의 값이 바뀌면 "props가 변경되면 하위 컴포넌트는 리-렌더링 된다"에 포함될까?

먼저 정답을 말하자면 리-렌더링이 되지 않는다.

당연한 것 아니냐? 라고 할 수 있다.

 

하지만 나는 처음에 "props가 변경이 되면 하위 컴포넌트는 리-렌더링 돼" 라는 조건에 꽂혀있었다.

그럼 useRef가 아무리 리-렌더링에 영향을 주지 않는다 하더라도 props가 변경되면 리-렌더링 되는 조건을 피할 수 없지 않을까? 했다.

 

아래 예제를 보자

let num = 0

const TestC = () => {
  const hi = useRef(0)

  const handleClick = () => {
    hi.current = num++
    console.log(hi.current)
  }

  return (
    <div>
      <button onClick={handleClick}>클릭</button>
      <TestComponent value={hi.current} /> //useRef의 current 값을 props로 내려주고 있음
    </div>
  )
}

const TestComponent = ({ value }) => {
   useEffect(() => {
    console.log('하위 컴포넌트 렌더링')
  })

  return (
    <div>
      <h1>하위 컴포넌트</h1>
      {value}
    </div>
  )
}

 

그저 상위 컴포넌트에서 useRef를 선언하고, 하위컴포넌트에 useRef의 current값을 내려주고 있는 코드다.

여기서 상위 컴포넌트의 버튼을 클릭하면 hi.current의 값은 변경된다.

 

이때 hi라는 ref 객체 자체를 내리는게 아니고 ref.current에 저장된 원시값을 내려주고 있다. - 이 부분에서 헷갈렸다.

그렇지만 위와 같이 리-렌더링이 전혀 되지 않는다.

ref 객체도 아니고 current 안의 원시값인데도 말이다.

 

이유는 아래와 같다고 한다.

React는 부모 컴포넌트에서 자식 컴포넌트로 전달된 props가 변경될 때 자식 컴포넌트를 리렌더링합니다.
그러나 useRef의.current 값이 변경되었을 때,
이 변경은 React의 상태 관리 시스템 밖에서 일어나므로 React가 자동으로 감지하지 못합니다.
그리고 이것이 바로 useRef의.current 값을 직접 변경하는 것이 props의 변화로 인식되지 않는 이유입니다.

그래서 챗지피티에게 계속 질문해봤다.

TestC 컴포넌트 내에서 hi.current의 값을 변경했을 때 TestComponent가 리렌더링되지 않는 핵심적인 이유는 React가 props의 변경을 감지하는 방식과 관련이 있습니다.
TestComponent의 props가 변경되면 그 컴포넌트는 원칙적으로 리렌더링되어야 합니다.
그러나, TestC에서 hi.current 값이 변경되어도, 이것이 TestC 컴포넌트의 상태(state)나 속성(props) 변경으로 간주되지 않습니다.
왜냐하면 useRef로 생성된 ref 객체의 .current 속성 변경은 React의 렌더링 시스템에서 추적되지 않기 때문입니다.
TestC에서 hi.current 값을 변경하더라도, 이 변경이 TestC의 재렌더링을 유발하지 않습니다.
그리고 React 컴포넌트는 부모 컴포넌트가 리렌더링될 때 자기 자신의 props가 변경되었는지를 확인하고, 변경이 있을 경우에만 리렌더링됩니다.

TestC에서 hi.current의 변경이 TestC의 재렌더링을 유발하지 않으므로, TestComponent에 전달되는 props가 변경되었다고 인식되지 않습니다.
결과적으로, TestComponent는 리렌더링되지 않습니다.

여기서 이제 답을 얻었다.

그니까 React 컴포넌트는 부모 컴포넌트가 리-렌더링 될 때! 자기 자신의 props가 변경되었는지를 확인하고
여기서 변경이 있을 때 리-렌더링하는 건데

useRef는 TestC(부모 컴포넌트)의 리-렌더링을 유발하지 않으니까
props변경 감지조차 할 필요없다고 판단하고 하위 컴포넌트도 리-렌더링을 하지 않는 것이다.

 

그래서 물어보니 아래와 같이 대답했다.

네, 맞습니다. React에서 하위 컴포넌트의 props 변경을 감지하고 리렌더링을 결정하는 과정은 부모 컴포넌트가 리렌더링 될 때 수행됩니다.

React 컴포넌트는 부모 컴포넌트가 리렌더링되면, 자신이 받는 props가 이전 렌더링 때와 비교하여 변경되었는지를 확인합니다. props에 변경이 있을 경우, 해당 하위 컴포넌트도 리렌더링됩니다.
반면, props에 변경이 없다면 리렌더링이 발생하지 않을 수 있습니다
(리액트의 PureComponent나 React.memo를 사용하는 경우와 같이).

useRef를 사용하는 경우, useRef는 생성된 ref 객체를 통해 값을 직접 수정할 수 있도록 해주지만, 이러한 변경이 React의 상태 관리 시스템에 의해 추적되지 않습니다.
즉, useRef로 current 속성의 값을 변경해도, 이는 React의 렌더링 시스템에 의해 상태 변경으로 간주되지 않으며, 따라서 자동으로 컴포넌트의 리렌더링을 유발하지 않습니다.

따라서 useRef로 current 값을 변경해도 부모 컴포넌트가 자동으로 리렌더링되지 않기 때문에, 이러한 변경이 하위 컴포넌트의 props 변경으로도 간주되지 않습니다.
하위 컴포넌트의 props 변경 감지는 부모 컴포넌트의 리렌더링 과정에서 이루어지므로, useRef를 사용해 변경된 값이 하위 컴포넌트에 바로 반영되길 원한다면, 다른 상태 관리 방법(예: useState, useReducer)을 사용하여 컴포넌트의 리렌더링을 명시적으로 유발해야 합니다.

이러한 궁금증을 문서에서 찾아보려고해도 잘 찾을 수가 없어서 이 궁금증은 챗지피티에서 의존해서 답을 찾았다.

이것 역시 답이 아닐 수 있다.

그렇지만 타당한 이유라고 생각됐다.

props의 변경 감지는 부모 컴포넌트가 리-렌더링 될 때 수행한다! 

라는 말이 어느정도 맞는 것 같다고 생각했다.

 

이 부분은 나중에 공식으로 답변한 글이 있다면 다시 수정하려고 한다.

 

728x90
반응형
LIST