[React] useState, useEffect 동작 만들어보기

오늘은 useState와 useEffect를 간단하게 만들어보는 실습을 진행했다.

클로저에대해 대강 이해는 해도 어떨 때 쓰는건지 잘 와닿지 않았는데 오늘 조금은 더 알게된 거 같다!

 

클로저

  • 함수와 그 함수가 선언될 때 렉시컬 환경의 조합
  • 함수가 생성될 당시의 외부 변수 상태를 기억하고, 이를 함수 호출시에도 계속 접근할 수 있게 해줌

 

function outer() {
  let count = 0; // inner함수가 선언될 때의 환경
  return function inner() {
    count++;
    console.log(`Count: ${count}`);
  }
}

const counter = outer();

counter(); // Count: 1
counter(); // Count: 2
counter(); // Count: 3

outer()의 실행이 마무리 됐기 때문에 그 내부의 count도 사라질 것이라 생각할 수 있지만, 반환받은 inner()가 선언될 때의 정보에 계속 접근할 수 있으므로 계속해서 count에 어떤 동작을 취할 수 있다.

 

 

useState

필요한 동작

const [state, setState] = useState(initialValue);
  • 초기화: 컴포넌트가 렌더링될 때 useState는 initialValue로 상태를 초기화
  • 상태 저장: React는 이 상태를 컴포넌트 인스턴스와 함께 저장, 이후에 상태가 변경되어도 값을 유지
  • 상태 업데이트: setState를 호출하면 React는 컴포넌트를 리렌더링하고 새로운 상태값을 적용

 

렌더 함수 만들기

const MyReact = {
  render(Component) {
    const Comp = Component();
    Comp.render(); // 컴포넌트의 렌더링 함수 실행
    currentHook = 0; // 컴포넌트가 렌더링되면 hook들을 처음부터 다시 실행하므로, 인덱스를 0으로 되돌려둠
    return Comp; // 컴포넌트의 렌더링 함수를 실행하고, 컴포넌트를 반환
  },
};

export default MyReact;
import MyReact from "./React.mjs";

function ExampleComponent() {
  return {
     render: () => console.log("render"),
  };
}

let App = MyReact.render(ExampleComponent); // 초기 렌더링

 

useState 만들기

let _val;
const useState = (initialValue) => {
  if (!_val) {
    _val = initialValue;
  }

  function setState(newVal) {
    _val = newVal;
  }
  return [_val, setState];
};

위와 같이 만드는 경우 useState를 여러번 사용했을 때 모든 값이 _val에 저장되기 때문에 문제가 된다.

따라서 hook들이 사용될 때 값을 관리해줄 공간이 필요하다. (배열)

 

export const useState = (initialValue) => {
  // hooks에 값이 있을 때는 그 값을, 없는 경우 initialValue를 hooks[currentHook]에 저장
  // 컴포넌트에서 useState를 이용해 처음 state를 선언했을 때가 아직 hooks[currentHook]에 저장된 값이 없을 때!
  hooks[currentHook] = hooks[currentHook] || initialValue;

  // 나중에 setState가 사용될 때를 위해 선언된 순서를 저장해둠
  // 위에서 currentHook을 사용하는 건 const [~,~] = useState(0); 했을 때는 순서대로 실행되니까 currentHook 바로 사용하면 되고
  // setState는 순서와 상관없이 컴포넌트 내부에서 언제 호출될지 모르니까 선언된 순서인 currentHook을 저장해두고 hookIndex를 사용??
  // 클로저가 있어서 setState가 선언될 때 hookIndex를 기억해둘 수 있다
  const hookIndex = currentHook;
  const setState = (newState) => {
    if (typeof newState === "function") {
      // 함수형변환 한 경우 전달받은 newState는 함수일 것이므로 그 함수를 활용해 새로운 값 할당
      hooks[hookIndex] = newState(hooks[hookIndex]);
    } else {
      hooks[hookIndex] = newState;
    }
  };

  // 나중에 선언해둔 state, setState에 접근할 수 있도록 반환
  // 다음에 호출할 hook의 호출 순서를 관리하기 위해 currentHook을 1증가
  return [hooks[currentHook++], setState];
};

 

 

useEffect

필요한 동작

useEffect(callback, deps);
  • Effect 처리: 컴포넌트가 렌더링된 이후, 부수적인 작업을 실행
  • 의존성 배열 관리: 의존성 배열을 통해 특정 값이 변경될 때만 이펙트가 다시 실행되도록 제어
  • 클린업 함수 처리: 이펙트가 다시 실행되기 전에 이전 이펙트의 클린업 작업을 실행할 수 있음

 

useEffect 만들기

const useEffect = (callback, depArray) => {
  // 의존성배열이 있는지 확인
  const hasNoDeps = !depArray;

  // 이전 Effect가 실행됐을 때의 의존성 배열/cleanUp 을 확인, 첫 실행이라면 undefined
  const prevDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined;
  const prevCleanUp = hooks[currentHook]
    ? hooks[currentHook].cleanUp
    : undefined;

  // 의존성배열에 변화가 있는지 확인
  // 전달받은 depArray를 순회하며 이전 실행 시점의 deps와 비교해 모두 같은 값을 가지고있는지 확인
  // every()로 모든 요소가 같은지 확인, 하나라도 다르면 !false === true
  // 값 자체로 비교하기 때문에 비교할 요소가 참조타입인 경우 [...state]와 같이 새로운 주소에 할당해줘야 하는 것!
  const hasChangedDeps = prevDeps
    ? !depArray.every((el, i) => el === prevDeps[i])
    : true;

  if (hasNoDeps || hasChangedDeps) {
    if (prevCleanUp) prevCleanUp(); // cleanUp이 있는 경우 이전 이펙트의 cleanUp을 먼저 실행

    // 전달받은 callback, depArray를 업데이트 해줌
    const cleanUp = callback();
    hooks[currentHook] = { deps: depArray, cleanUp };
  }

  currentHook++;
};

 

 

전체 코드

// MyReact.js
let hooks = []; // hook을 사용할 때 필요한 여러 값들을 저장해둘 배열
let currentHook = 0; // 현재 실행되고있는 hook의 순서, 이 값을 인덱스로 활용해 위 배열에서 값을 가져오거나 관리한다

const useState = (initialValue) => {
  // hooks에 값이 있을 때는 그 값을, 없는 경우 initialValue를 hooks[currentHook]에 저장
  // 컴포넌트에서 useState를 이용해 처음 state를 선언했을 때가 아직 hooks[currentHook]에 저장된 값이 없을 때!
  hooks[currentHook] = hooks[currentHook] || initialValue;

  // 나중에 setState가 사용될 때를 위해 선언된 순서를 저장해둠
  // 위에서 currentHook을 사용하는 건 const [~,~] = useState(0); 했을 때는 순서대로 실행되니까 currentHook 바로 사용하면 되고
  // setState는 순서와 상관없이 컴포넌트 내부에서 언제 호출될지 모르니까 선언된 순서인 currentHook을 저장해두고 hookIndex를 사용??
  // 클로저가 있어서 setState가 선언될 때 hookIndex를 기억해둘 수 있다
  const hookIndex = currentHook;
  const setState = (newState) => {
    if (typeof newState === "function") {
      // 함수형변환 한 경우 전달받은 newState는 함수일 것이므로 그 함수를 활용해 새로운 값 할당
      hooks[hookIndex] = newState(hooks[hookIndex]);
    } else {
      hooks[hookIndex] = newState;
    }
  };

  // 나중에 선언해둔 state, setState에 접근할 수 있도록 반환
  // 다음에 호출할 hook의 호출 순서를 관리하기 위해 currentHook을 1증가
  return [hooks[currentHook++], setState];
};

const useEffect = (callback, depArray) => {
  // 의존성배열이 있는지 확인
  const hasNoDeps = !depArray;

  // 이전 Effect가 실행됐을 때의 의존성 배열/cleanUp 을 확인, 첫 실행이라면 undefined
  const prevDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined;
  const prevCleanUp = hooks[currentHook]
    ? hooks[currentHook].cleanUp
    : undefined;

  // 의존성배열에 변화가 있는지 확인
  // 전달받은 depArray를 순회하며 이전 실행 시점의 deps와 비교해 모두 같은 값을 가지고있는지 확인
  // every()로 모든 요소가 같은지 확인, 하나라도 다르면 !false === true
  // 값 자체로 비교하기 때문에 비교할 요소가 참조타입인 경우 [...state]와 같이 새로운 주소에 할당해줘야 하는 것!
  const hasChangedDeps = prevDeps
    ? !depArray.every((el, i) => el === prevDeps[i])
    : true;

  if (hasNoDeps || hasChangedDeps) {
    if (prevCleanUp) prevCleanUp(); // cleanUp이 있는 경우 이전 이펙트의 cleanUp을 먼저 실행

    // 전달받은 callback, depArray를 업데이트 해줌
    const cleanUp = callback();
    hooks[currentHook] = { deps: depArray, cleanUp };
  }

  currentHook++;
};

const MyReact = {
  render(Component) {
    const Comp = Component();
    Comp.render(); // 컴포넌트의 렌더링 함수 실행
    currentHook = 0; // 컴포넌트가 렌더링되면 hook들을 처음부터 다시 실행하므로, 인덱스를 0으로 되돌려둠
    return Comp; // 컴포넌트의 렌더링 함수를 실행하고, 컴포넌트를 반환
  },
};

MyReact.useState = useState;
MyReact.useEffect = useEffect;

export { useState, useEffect };
export default MyReact;
// index.js
import MyReact, { useState, useEffect } from "./MyReact.js";

function ExampleComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("foo");

  useEffect(() => {
    // callback
    console.log("effect", count, text);
    return (
      // callback의 cleanUp
      () => {
        console.log("cleanup", count, text);
      }
    );
  }, [count, text]);

  return {
    // 함수형 컴포넌트가 반환하는 객체
    click: () => setCount(count + 1),
    type: (text) => setText(text),
    noop: () => setCount(count),
    render: () => console.log("render", { count, text }),
  };
}

// 초기 렌더링
let App = MyReact.render(ExampleComponent);

App.click();
App = MyReact.render(ExampleComponent);

App.type("bar");
App = MyReact.render(ExampleComponent);

App.noop();
App = MyReact.render(ExampleComponent);

App.click();
App = MyReact.render(ExampleComponent);