Front-End/JavaScript

Vanilla JavaScript로 React Hooks 구현하기

Voyage_dev 2024. 3. 25. 00:18

 

Hook은 React 16.8부터 도입된 기능으로, 기존의 class component에서 벗어나 functional component로 상태를 관리하고 react의 다른 기능을 사용할 수 있게 한다. 하지만 어떤 원리로 동작하는 걸까? 어떻게 컴포넌트가 계속 리렌더링되고 변경되는 것과 무관하게 상태를 관리하고 가져올 수 있는 걸까?

 

답은 Closure!!

JavaScript Closure

  • lexical environment와 함수의 조합으로 함수 내부의 lexical scope에서 정의한 변수는 함수 바깥에서는 직접적으로 참조하거나 수정할 수 없다

[JavaScript] Closure (클로저)

Vanilla JavaScript로 useState 구현

  • 기본적인 구조는 useState를 사용하는 컴포넌트의 바깥에서 즉, 외부 스코프에서 모든 상태 변수를 정의하고 관리하여 특정 상태 값을 유지하고 변경할 수 있도록 하는 것이다.
  • React를 하나의 큰 함수로 본다면, React라는 함수의 lexical scope 안에서 state에 대한 변수를 선언, 이 state에 대한 변수를 참조하거나 변경할 수 있는 useState 함수를 React로부터 제공받아 관리할 수 있다

하나의 상태만 관리한다고 하면 간단하게 구현할 수 있다.

export function createHooks(callback) {
  let state; // 관리하고자 하는 상태 (클로저)
  
  function useState(initialValue) {
    // 최초로 선언할 때만 할당
		if(!state) state = initialValue
    
    // state를 변경할 수 있는 setState 함수
    const setState = (newState) => {
      // setState 호출 시 newValue로 변경
      state = newState;
      // state가 변경될 때마다 callback 함수 실행
      callback(); 
    };
    
    return [state, setState];
  }

  return { useState };
}
  • 상태를 사용하는 state는 클로저로 외부에 선언

여러 상태를 가질 수 있는 useState 구현

// 배열 사용
export function createHooks(callback) {
  let idx = 0; // useState가 실행 된 횟수
  let stateArr = []; // state를 보관할 배열

  const useState = (initState) => {
    const currIdx = idx;
		
		// 초기값 설정
    if (stateArr.length === currIdx) {
      stateArr.push(initState);
    }
		
		//  state 할당
    const state = stateArr[currIdx];

    const setState = (newState) => {
      // 직접 수정하는 것이 아닌, 내부의 값을 새로운 값으로 할당
      stateArr[currIdx] = newState;
      callback();
    };
			// 끝나고 나면 실행 된 횟수를 1올려 stateArr 다음 값으로 push 할 수 있게 
	    idx++;
	    return [state, setState];
	  }
	  
    return { useState };
  };
  
  - stateArr 배열에 idx라는 인덱스로 접근해서 초기 상태들을 리턴 해 줍니다 stateArr배열 길이는 useState를 사용할 때마다 +1이 된다.
	- 상태 업데이트 역시 클로저로 선언된 idx로 접근애서 해당 useState의 상태인 stateArr[idx]를 수정한 다음 리렌더링을 진행한다.
  
 
 // 객체 사용
 export function createHooks(callback) {
  let idx = 0; // useState가 실행된 횟수
  let stateObj = {}; // 상태를 보관할 객체

  const useState = (initState) => {
    const currIdx = idx;

    // 초기값 설정
    if (!stateObj.hasOwnProperty(currIdx)) {
      stateObj[currIdx] = initState;
    }

    // 상태 할당
    const state = stateObj[currIdx];

    const setState = (newState) => {
      // 새로운 상태를 객체에 할당
      stateObj[currIdx] = newState;
      callback();
    };

    // 실행된 횟수를 1 증가시켜 다음 값으로 push 할 수 있게 함
    idx++;
    return [state, setState];
  }

  return { useState };
};

- 상태를 객체로 관리하고, 각 상태는 객체의 특정 키에 매핑된다. 객체를 사용함으로써 각 상태에 보다 명시적인 키를 지정할 수 있게 됐다.
- 가독성을 향상 시키고, 인덱스를 관리하는 부분을 배열보다는 더 간단하게 처리할 수 있다.
- 객체의 속성을 문자열로도 사용할 수 있다.

배열 사용

장점

  • 간단하고 직관적이다.
  • useState 훅이 반환하는 배열과 유사한 방식으로 상태를 다룰 수 있다.

단점

  • 배열의 각 요소는 인덱스를 기반으로 하기 때문에 각 상태에 명시적인 이름을 부여할 수 없어서 가독성이 좋지 않다.

객체 사용

장점

  • 각 상태에 명시적인 키를 지정하여 가독성을 높일 수 있다.
  • 상태의 추가/제거가 발생해도 인덱스를 조정할 필요가 없다.

단점

  • 상태를 업데이트할 때 키를 사용해야 하기 때문에 코드가 복잡해 질 수도 있다.

useMemo 구현

 export function createHooks(callback) {
  let idx = 0; 
  let stateObj = {}; 
  const cacheValue = {};

  const useState = (initState) => {
		// ...이하 동일
  }
  
    const useMemo = (fn, refs) => {
    // 인자로 받은 deps 배열을 문자열로 변환하여 캐시 키로 사용
    const key = JSON.stringify(refs);

    // 만약 cacheValue 저장된 결과가 있다면 이를 반환
    if (key in cacheValue) {
      return cacheValue[key];
    }

    // cacheValue 저장된 결과가 없다면 새로운 결과를 계산하고 캐시에 저장한 후 반환
    const result = fn();
    cacheValue[key] = result;
    return result;
  };

  return { useState, useMemo };
};

// or

  const useMemo = (fn, refs) => {
    // 캐시 객체 생성
    useMemo.cache = useMemo.cache || {};

    // 인자로 받은 deps 배열을 문자열로 변환하여 캐시 키로 사용
    const key = JSON.stringify(refs);

    // // 만약 캐시에 저장된 결과가 있다면 이를 반환
    if (key in useMemo.cache) {
      return useMemo.cache[key];
    }
    const result = fn();
    useMemo.cache[key] = result;
    return result;
   };

위에 코드는 일반적인 객체를 만들고 결과값을 비교 후 저장 혹은 반환하는 형태이고 얘기하고 싶은 부분은 밑에 함수 .cache 객체이다.

useMemo 함수의 .cache 속성은 결과를 캐시하는 데 사용되는 객체이다. 즉, useMemo 함수가 호출될 때마다 새로운 값을 계산하고 이를 저장하는 데 활용된다. 즉, 이 객체에는 계산된 결과가 저장되어 동일한 인자로 함수가 호출될 때 다시 계산하지 않고 이전에 계산된 결과를 반환할 수 있게 한다.

 

.cache 객체의 역할

  • 이전 결과 저장 : 새로운 인자로 함수가 호출될 때마다 이전에 계산된 결과를 지정
  • 결과 확인 : 동일한 인자로 함수가 호출될 때마다 캐시에 저장된 결과를 확인하여 이전에 계산된 결과가 있으면 반환
  • 최적화 : 함수의 재 계산을 피하며 성능을 최적화

캐시 객체는 함수가 호출될 때마다 계속해서 참조되기 때문에 함수의 스코프 내에서만 접근할 수 있다. 즉, 함수 내부에서만 사용할 수 있다.

 

 

 

 

 

 

Reference

https://velog.io/@aborile/React-Hooks-with-Vanilla-JavaScript

 

React Hooks - vanilla JavaScript로 구현하기

Hook은 React 16.8부터 도입된 기능으로, React를 배우거나 사용해 본 사람이라면 누구나 써봤고, 또 많이 쓰고 있을 기능일 텐데, 이 Hook은 대체 어떤 원리로 동작하는 걸까? 어떻게 컴포넌트가 계속

velog.io

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/#_3-render-%E1%84%92%E1%85%A1%E1%86%B7%E1%84%89%E1%85%AE-%E1%84%8E%E1%85%AE%E1%84%89%E1%85%A1%E1%86%BC%E1%84%92%E1%85%AA

 

Vanilla Javascript로 React UseState Hook 만들기 | 개발자 황준일

Vanilla Javascript로 React UseState Hook 만들기 본 포스트는 React의 useState Hook의 작동방식에 대해 고민해보고, 구현해보고, 최적화하는 내용을 다룹니다. 1. React의 useState (1) 의문을 갖기 필자는 React를 사

junilhwang.github.io