본문 바로가기
JAVASCRIPT/리액트

리액트 커스텀 훅 사용 시 다중상태관리와 클로저의 존재 이유

by 진짠 2024. 6. 12.
728x90

리액트의 컴포넌트

  • 리액트의 컴포넌트는 함수 클래스로 나뉜다.
  • 요새는 함수 컴포넌트를 사용하는 추세라고 한다. 왜냐하면 클래스 컴포넌트는 변경에 취약하여 인스턴스로 만든 속성에 접근이 가능한 반면 함수는 내부 상태를 클로저로 관리하기 때문에 외부에서 접근하지 못한다.
  • 또한 클래스 컴포넌트는 리액트 컴포넌트를 반드시 상속 받아야하고 생성자 super 호출 및 render메소드를 반드시 구현해야함.
  • 함수 컴포넌트는 리액트 엘리먼트를 반환해야함.
  • 클래스 컴포넌트는 상태를 관리하기 위해 this 객체를 사용한다.
  • 반면 함수 컴포넌트는 상태관리에 한계가 있기 때문에 등장한 것이 훅(hook)이다.

훅(hook)

  • 리액트의 useState를 사용하여 함수 컴포넌트의 state값을 변경한다. (클래스의 인스턴스의 경우 this.state를 통해 내부의 값을 변경할 수 있지만 함수의 경우 한번 실행되고 종료되는 특성 상 훅을 사용해야 한다.) -> 리랜더링 하는 과정이라고도 할 수 있다.

다중 상태 관리와 커스텀 훅

  • 그렇다면 여러개의 상태를 관리해야 할때는 어떻게 할까?
  • 해당 값을 가리키는 포인터 역할이 필요하다.
  • 예제 소스는 인프런 김정환 선생님 것을 참고하였다.
**App.jsx**
function NameField() {
    // 최초 랜더링 시 firstName과 lastName을 생성할때 MyReact의 useState를 호출해서 값을 세팅해준다.
    // 이때 값은 firstName = “사용자1”, lastName = “김” 이 될 것이다.
    const [firstName, setFirstName] = MyReact.useState("사용자1");
    const [lastName, setLastName] = MyReact.useState("김");
		
    // input 값 변경 시 실행되는 함수다. MyReact를 통해 생성된 set함수를 사용한다.
    const handleChangeFirstName = (e) => {
        setFirstName(e.target.value);
    }
    
    // input 값 변경 시 실행되는 함수다. MyReact를 통해 생성된 set함수를 사용한다.
    const handleChangeLasttName = (e) => {
        setLastName(e.target.value);
    }

    return <>
        <input value={firstName} onChange={handleChangeFirstName} />
        <input value={lastName} onChange={handleChangeLasttName} />
    </>
}

export default () => <NameField />;
**MyReact.jsx**
import React from "react";
const MyReact = (function MyReact() {
    const memorizedStates = [];
    const isInitialized = [];
    // cursor를 유의하며 본다. 
    // 이것이 다중 상태에서 각각의 객체를 판단할 수 있는 포인터 역할을 한다.
    let cursor = 0;

    function useState(initialValue = "") {
        const { forceUpdate } = useForceUpdate();
        // 최초 실행 시 isInitialized 변수를 이용하여 값을 세팅해준다. 
        // 다중 상태이기 때문에 이 변수 또한 배열로 관리해준다.
        if(!isInitialized[cursor]) {
            memorizedStates[cursor] = initialValue;
            isInitialized[cursor] = true;
        }
        
        // state 변수에 최초로 생성될 당시 세팅된 값을 확인하는 역할을 한다.
        const state = memorizedStates[cursor]
				
        // setState의 구현 부분이다. _cursor를 유의하며 봤다. 
        // 최초 세팅 시 생성한 cursor값은 클로저로 세팅하여 setState안에 저장할 것이다.
        // 그래야지 해당 setState를 호출했을 때 저장했던 cursor값을 확인하여 해당 값을 바꿀 수 있기 때문이다.
        // 미리 저장한 cursor값으로 배열을 확인해서 변경된 값(nextState)으로 수정해준다.
        // 그리고 재랜더링 단계를 통해 값을 그려준다.
        const setStateAt = (_cursor) => (nextState) => {
            if(state === nextState) return;
            memorizedStates[_cursor] = nextState;
            forceUpdate(); // 재렌더링
        };
        // setState를 사용시 setStateAt함수를 사용하며 이때의 cursor값을 가져가기 위해 클로저로 적용한다.
        const setState = setStateAt(cursor);
				
        // 차례대로 cursor값을 저정하기 위해 더해준다.
        cursor = cursor + 1;

        return [state, setState];
    }

    function useForceUpdate() {
        const [value, setValue] = React.useState(1);
        // 리액트의 상태가 바뀌면 강제로 재렌더링 된다는 것을 이용하여 React.useState로 값을 설정하고
        // 그 값을 바꿔줌으로써 재렌더링한다.
        // cursor는 0으로 초기화해야 재렌더링 시 차례대로 커서가 증가하며 저장했던 memorizedState에서
        // 값을 꺼낼 수 있다.
        const forceUpdate = () => {   
            setValue(value + 1);
            cursor = 0; 
        }
        return { forceUpdate };
    }

    return { useState };
})();

export default MyReact;
  • 소스의 순서를 살펴본다.
    -> App.jsx의 NameField를 최초 렌더링한다.
    -> function NameField의 firstName, lastName을 MyReact.useState를 이용해 세팅한다.
    -> MyReact의 function useState로 이동한다.
    -> memorizedStates[cursor]에 초기값을 세팅한다.
    -> const setState = setStateAt(cursor); 를 통해 cursor값을 클로저로 저장한다.
    -> if(state === nextState) return; 초기값이므로 별다른 재렌더링 로직을 타지않고 바로 빠져나온다.
    -> cursor = cursor + 1; 커서값을 증가시켜 배열에 차례대로 값을 넣어준다.
    -> 다시 App.jsx로 돌아온다. 여기까지가 최초 페이지 로딩 시 과정이다.

-> 다음과 같이 value값이 들어가 있는 것을 알 수 있다.
-> 이제 onchange를 테스트하기 위해 텍스트값을 변경한다.
-> 두번째 텍스트를 변경했다고 가정하자.
-> handleChangeLasttName 가 호출되고 이는 setLastName을 호출한다.
-> e.target.value는 내가 현재 입력한 텍스트 값을 가져온다.
-> setLastName은 최초 렌더링 시 클로저인 cursor의 1에 저장되어 있다는 것을 기억하고 있다.

내가 이해를 못한 부분

  • cursor는 MyReact.jsx에 선언되어있는 전역변수인데 어떻게 setState호출 시 기억한 값을 가져오는걸까? 클로저는 알겠는데 어떻게 적용이 되는거지?
  • 혼자 이해의 물꼬를 튼 부분은 함수마다 변수를 저장하는 스코프가 다르다는 부분이었다.
  • 최초 렌더링 시에는 cursor의 전역변수를 사용할 것이다. 그리고 1씩 증가하는 로직을 통해 순서대로 배열의 값에 저장될 것이다.
  • 주의해서 봐야할 부분은 'const setState = setStateAt(cursor);' 이다.
  • 이를 통해 최초 렌더링 시 cursor값은 '캡쳐' 된다. setState함수 자체에 cursor값이 저장되는 것으로 이해했다. 왜냐하면 setState호출은 곧 setStateAt에 저장된 cursor까지 같이 불러오니까.
  • 이것은 setStateAt에서 _cursor 인수를 통해 로직으로 처리되고 재호출 시에도 마찬가지일 것이다.
  • 클로저를 사용안하면 다음과 같은 코드가 된다.
    const setState = (nextState) => {
    if(state === nextState) return;
    memorizedStates[cursor] = nextState;
    forceUpdate(); // 재렌더링
    };
  • 해당 cursor변수를 선언하고 초기화하는 어떤 과정도 없기 때문에 전역변수인 cursor를 쓸 것이고 당연히 얘는 별도로 관리되지 않기 때문에 포인터의 역할을 할 수 없다.
  • 이해를 마치고 다시 과정으로 넘어갔다.

-> cursor값을 참조하여 해당하는 값에 접근한 다음 변경한다.
-> 재랜더링한다.(forceUpdate();)
-> const [value, setValue] = React.useState(1); 이부분을 참고한다. 리액트는 상태가 바뀌면 강제적으로 재랜더링 되는 규칙을 이용한다. 이를 통해 값을 바꾸고 재랜더링 시킨다.
-> cursor값을 0으로 초기화한다. 그래야 재랜더링 시 저장해놓은 값(memorizedStates[cursor]) 에 순서대로 접근하여 가져올 수 있기 때문이다.
-> 재랜더링을 통해 변경된 값이 const state = memorizedStates[cursor] 여기에 차례대로 담긴다.
-> 반환된 state는 {lastName} 에 반영된다.

  • 다음과 같은 과정을 통해 값을 바꾸면 value의 값이 바뀌는 것을 알 수 있었다. 

(선생님의 훌륭한 강의를 들으며 어려운 리액트를 쉽게 접근하고 배울 수 있어 감사드린다.)

728x90

댓글