FrontEnd/[리액트 2부] 고급 주제와 훅

[3.1장 클래스/함수 컴포넌트][3.2장 상태훅]

Tony Lim 2024. 3. 11. 09:20
class Contract {
  constructor(name) {
    this.name = name;
  }

  sign() {
    const capturedName = this.name;
    setTimeout(() => console.log("서명인: ", capturedName), 3000);
  }
}

// const contract = new Contract("user");
contract.sign();
contract.name = "user2";

function createContract(name) {
  const sign = () => {
    setTimeout(() => console.log("서명인: ", name), 3000);
  };

  return {
    sign,
  };
}

const contract = createContract("user3")
contract.sign()

Class는 변수의 상태를 변경할 수 있는 여지가 있어서 안에 있는 메소드들이 영향을 받을 수 있다.

이를 방지하기 위해 closure를 사용해서 function scope안에 변수를 선언하는 방법이다.

sign은 선언된 시점의 lexical environment를 기억한다. 이떄 createContract("user3")을 호출했을 때 sign이 선언이 됨으로 name을 기억할 수 있다.

마찬가지로 리액트에서도 클래스 컴포넌트를 쓰게 되면 props 가 의도치 않게 변경될 위험이 있다.

따라서 요즘 리액트에서는 함수 컴포넌트를 권장한다. 

  • this 또한 class component에서는 bind를 사용해야할 때가 존재하지만 function component 에서는 this가 존재하지 않는다.
  • class component는 생명주기에 따라 로직을 작성하게 된다. mount ,unmount 시점에 각각 고려해야할 것 들이 존재한다.
  • function component 는 생명주기가 없고 한번 실행되면 끝이 나버린다.

상태 훅

function NameField() {
  const name = "user1"
  const handleChange = (e) => {
    // name 참조 불가능
    name = "user2"
    console.log(name)
  }
  return <input value={name} onChange={handleChange}></input>
}

export default () => <NameField></NameField>

한번 렌더링된 NameField의 name의 값을 변경할 수 가 없다. state 같은것이 필요한 시점이다.

const MyReact = (function MyReact() {
  let firstName;
  let isInitialized = false;
  function useName(initialValue = "") {
    const { forceUpdate } = userForceUpdate();

    if (!isInitialized) {
      firstName = initialValue;
      isInitialized = true;
    }

    const setFirstName = (value) => {
      if (firstName === value) return;
      firstName = value;
      console.log("setFirstName = ", firstName)
      forceUpdate();
    };
    console.log("firstName = " , firstName)
    return [firstName, setFirstName];
  }

  function userForceUpdate() { // for rendering purpose
    const [value, setValue] = React.useState(1);
    const forceUpdate = () => setValue(value + 1);
    return { forceUpdate };
  }

  return {
    useName,
  };
})();

export default MyReact;

useName을 innerfunction으로 즉시실행함수에서 return 하고 있다. useName이 선언된 시점의 lexicalscope이 저장이되니

useName function안의 setFirstName이 참조하는 firstName은 지속적으로 참조가 가능하다.

userForceUpdate는 re rendering을 위한 목적이지 여기서 중요한 부분이 아니다.

function NameField() {
  const [firstName, setFirstName] = MyReact.useName("user1");
  const handleChange = (e) => {
    setFirstName(e.target.value);
    console.log(firstName)
  };
  return <input value={firstName} onChange={handleChange}></input>;
}

export default () => <NameField></NameField>;

useName은 firstName, setFirstName 을 tuple로 반환을 하게 된다. 

onChange에 setFirstName이 호출되고 name 값이 변경이되고 -> forceUpdate를 통해 re rendering이 진행된다.

하지만 현재로써는 변수 하나만 state로 관리가 되고 2개이상은 관리할 수 없다.

const MyReact = (function MyReact() {
  let memorizedStates = [];
  let isInitialized = [];
  function useState(cursor ,initialValue = "") {
    const { forceUpdate } = userForceUpdate();

    if (!isInitialized[cursor]) {
      memorizedStates[cursor] = initialValue;
      isInitialized[cursor] = true;
    }

    const state = memorizedStates[cursor]

    const setState = (nextState) => {
      if (state === nextState) return;
      memorizedStates[cursor] = nextState;
      forceUpdate();
    };
    return [state, setState];
  }

  function userForceUpdate() {
    // for rendering purpose
    const [value, setValue] = React.useState(1);
    const forceUpdate = () => setValue(value + 1);
    return { forceUpdate };
  }

  return {
    useState: useState,
  };
})();

export default MyReact;

state 자료구조를 배열로 변경하고 cursor를 통해 여러개의 state를 관리할 수 있도록 변경했다.

const MyReact = (function MyReact() {
  let memorizedStates = [];
  let isInitialized = [];
  function useState(cursor ,initialValue = "") {
    const { forceUpdate } = userForceUpdate();

    if (!isInitialized[cursor]) {
      memorizedStates[cursor] = initialValue;
      isInitialized[cursor] = true;
    }

    const state = memorizedStates[cursor]

    const setState = (nextState) => {
      if (state === nextState) return;
      memorizedStates[cursor] = nextState;
      forceUpdate();
    };
    return [state, setState];
  }

  function userForceUpdate() {
    // for rendering purpose
    const [value, setValue] = React.useState(1);
    const forceUpdate = () => setValue(value + 1);
    return { forceUpdate };
  }

  return {
    useState: useState,
  };
})();

export default MyReact;

2개이상의 state를 관리하기가 용이해졌다.

const MyReact = (function MyReact() {
  const memorizedStates = [];
  const isInitialized = [];
  let cursor = 0;
  function useState(initialValue = "") {
    const { forceUpdate } = userForceUpdate();

    if (!isInitialized[cursor]) {
      memorizedStates[cursor] = initialValue;
      isInitialized[cursor] = true;
    }

    const state = memorizedStates[cursor];

    const setStateAt = (_cursor) => (nextState) => {
      if (state === nextState) return;
      memorizedStates[_cursor] = nextState;
      console.log("_cursor =", _cursor, nextState);
      forceUpdate();
    };
    const setState = setStateAt(cursor);
    console.log(cursor, state);
    cursor += 1;
    return [state, setState];
  }

  function userForceUpdate() {
    // for rendering purpose
    const [value, setValue] = React.useState(1);
    const forceUpdate = () => {
      setValue(value + 1);
      cursor = 0
    };
    return { forceUpdate };
  }

  return {
    useState: useState,
  };
})();

export default MyReact;

하지만 사용하는쪽에서 cursor를 굳이 주입해줄 필요없이 react hook에서 관리하는 쪽이 낫다.

forceUpdate를 통해 re rendering 될때마다 MyReact.useState 가 매번 재호출된다. 이때마다 cursor는 처음 주입된 순서를 유지해야한다. 
현재 2개 이니 0, 1 이런식으로 re rendering 될 때마다 cursor의 값이 0, 1이 되어야한다.

이를 위해 setState에서 _cursor를 사용하게 함으로 closure에 lexical scope에 setState가 선언될때 주입받은 값을 저장하도록 한다.

또한 setStateAt(cursor) 에서 cursor의 값이 re rendering 될 때마다 증가하면 안됨으로 re render를 해주는 forceUpdate 시점에서 cursor =0 으로 하여 0,1 이  유지 되도록 한다.


useState의 역할은 함수 컴포넌트 안에서 지속할 수 있는 값, 상태를 관리할 수 있는 방법을 제공한다.

뿐만 아니라 화면을 re render 하는 역할도 한다.

이러한 훅을 쓸떄는 함수 컴포넌트 본문 최상단에서 호출해야한다. 컴포넌트가 실행될 때 훅이 순서대로 호출되어야만 리액트는 훅이 사용하는 상태를 배열에서 제대로 찾을 수 있기 때문이다.
조건문 안에 위치해서 실행되지않는 경우가 생기면 리액트는 state를 제대로 관리 할 수 없다.