[React] Hooks

유다송·2023년 8월 21일
0

React

목록 보기
13/14
post-thumbnail

등장 배경

  • React 16.8 버전 (2019년도) 에 추가된 공식 라이브러리
  • Class형 컴포넌트에서만 쓸 수 있었던 state와 life cycle을 Function형 컴포넌트에서도 사용 가능
  • 현재 공식문서에서는, Class형 컴포넌트보다는 Function형 컴포넌트로 새로운 React 프로젝트를 만들기를 권장

왜 사용하는지?

  • 클래스형 컴포넌트들은 method의 개념이므로, 리렌더링이 되더라도 render() 를 제외한 나머지 method 및 state는 그대로 보존이 되어 있음.

  • 함수형 컴포넌트들은 기본적으로 리렌더링이 될때, 함수 안에 작성된 모든 코드가 다시 실행되며 함수형 컴포넌트들이 기존에 가지고 있던 상태(state)를 전혀 관리(기억)할 수 없게 만듦
    ⇒ Stateless Component

  • 단순하게 React에서의 state 만을 의미하는 것이 아닌, 함수내에 써져 있는 모든 코드 및 변수를 기억할 수 없다는 의미
    ⇒ 함수형 컴포넌트가 리렌더링될때 무조건 새롭게 선언 & 초기화 & 메모리에 할당이 됨

  • 하지만 Hook의 등장으로, 브라우저에 메모리를 할당 함으로써, 함수형 컴포넌트가 상태(state)를 가질 수 있게 한 것. 즉, 함수 내에 써져있는 코드 및 변수를 기억할 수 있게 됐음을 의미

공식홈페이지에 따르면 Hook을 만든 이유는 다음과 같다.

1) 컴포넌트 사이에서 상태 로직 재사용의 어려움 -> render props, HOC 등
2) 복잡한 (클래스형) 컴포넌트들은 이해하기 어려움 -> 각종 생명주기 함수들
3) 클래스자체 개념을 이해하기 어려움 -> this 등

주의사항

  • Hook은 브라우저의 메모리 자원을 사용하기에 함부로 남발하면 오히려 성능저하를 불러올 수있다.

    💡 추가로 공부할 것 : Hook의 성능 최적화 방법

동작원리

  • React에서 Hook을 구현하는 핵심 원리는 바로 JavaScript의 클로저(Closure)이다.
  • Closure 기본 개념

클로저는 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능을 뜻한다.

function useFoo() {
	let foo = 0;
    
    function getFoo() {
    	return foo;
    }
    
    function setFoo(value) {
    	foo = value
    }
    
    return [getFoo, setFoo];
}

// 사용하는 측

const [getFoo, setFoo] = useFoo();

console.log(getFoo()); // 0

setFoo(1);

console.log(getFoo()); // 1
  • 일반적으로 함수가 반환하고 나면 그 함수 내에 선언된 지역 변수들은 메모리 상에서 사라지지만, 위 예시에서 foo라는 지역 변수는 useFoo() 함수가 반환하고 나서도 클로저라는 별도의 메모리 공간에 남아있게 된다.
  • 이는 useFoo() 함수가 반환하는 getFoo() 함수와 setFoo() 함수가 그 지역 변수를 사용하기 때문이다.
  • 이러한 경우 foo라는 지역 변수가 getFoo() 함수와 setFoo() 함수에 의해 붙잡혔다고 표현하기도 한다.

Closure를 바탕으로 useState() 구현해보기

const useState = (initialValue) => {
  let value = initialValue;
  
  const state = () => value;

  const setState = (newValue) => {
    value = newValue;
  };
  
  return [state, setState];
};


const [counter, setCounter] = useState(0);

console.log(counter()); // 0
setCounter(1);
console.log(counter()); // 1

문제점

  • 다만 state가 getter 방식의 함수로 구현됐다. 실제 useState와 동일하게 만들려면 state를 함수가 아닌 변수로 선언해야 한다.

💡 추가적으로 공부해야 할 것 : 각 Hooks의 세부적인 동작원리

const useState = (initialValue) => {
  let state = initialValue;

  const setState = (newValue) => {
    state = newValue;
  };
  
  return [state, setState];
};


const [counter, setCounter] = useState(0);

console.log(counter); // 0
setCounter(1);
console.log(counter); // 0 → Error!

문제점

  • state는 변수이기 때문에 useState로 리턴된 순간 더이상 변경할 수 없는 상태가 된다.
  • 우리는 state를 변수로 표현하면서도 상태 값을 유지하도록 만들어야한다.
  • 리액트는 state를 useState의 외부에 선언함으로써 이 문제를 해결한다.
const MyReact = (function () {
  let state;

  return {
    render(Component) {
      const Comp = Component();
      Comp.render();
      return Comp;
    },

    useState(initialValue) {
      state ||= initialValue;

      const setState = (newValue) => {
        state = newValue;
      };

      return [state, setState];
    },
  };
})();

const Counter = () => {
  const [count, setCount] = MyReact.useState(0);

  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count }),
  };
};

let App;
App = MyReact.render(Counter); // render: { count: 0 }
App.click();
App = MyReact.render(Counter); // render: { count: 1 }

문제점

  • useState를 사용하는 컴포넌트가 여러 개라면 문제가 생긴다.
  • 하나의 state 변수에 여러 컴포넌트가 접근하기 때문에 모든 컴포넌트의 state가 동일해지기 때문.
  • 리액트는 state를 useState 외부에 배열 형식으로 관리하여 이 문제를 해결.
let state = [];
let setters = [];
let cursor = 0;
let firstrun = true;

const createSetter = (cursor) => {
  return (newValue) => {
    state[cursor] = newValue;
  };
};

const useState = (initialValue) => {
  if (firstrun) {
    state.push(initialValue);
    setters.push(createSetter(cursor));
    firstrun = false;
  }

  const resState = state[cursor];
  const resSetter = setters[cursor];
  cursor++;

  return [resState, resSetter];
};

규칙

1. 최상위(at the Top Level)에서만 hook을 호출해야 합니다.

  • 반복문, 조건문 혹은 중첩 함수에서 hook을 호출하면 안 된다.
  • state는 컴포넌트의 실행 순서대로 배열에 저장될 것.
  • 만약 조건문 등을 만나면 컴포넌트의 실행 순서가 달라질 수 있다.

2. 오직 React 함수 내에서 hook을 호출해야 합니다

  • Hook을 일반적인 JavaScript 함수에서 호출하면 안 된다.
  • 함수 컴포넌트, 커스텀 훅 내에서만 호출할 수 있다.

두 규칙을 따랐을 때 컴포넌트가 렌더링 될 때마다 동일한 순서로 hook이 호출되는 것을 보장.

Hooks을 사용함으로써 얻을 수 있는 장점

  • 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있다.
  • 코드 양이 적지만 명료함을 잃지 않는다. (useSomething )
  • 상태관리 로직의 재활용이 가능하다.

[React] useState의 동작 원리와 클로저

Hook의 규칙

[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?

0개의 댓글