[React] - Closure와 상태관리 Hooks

이한형·2022년 10월 28일
1

Hooks

React Hooks는 상태관리 및 생명주기 API 등 기존의 클래스형 컴포넌트에서만 사용할 수 있던 기능들을 함수형 컴포넌트에서도 사용할 수 있도록 도와줍니다. 클래스형 컴포넌트에 비해서 함수형 컴포넌트는 선언과 사용하기가 편리하고 코드의 가독성이 좋아지고 메모리 사용량에서도 이점이 있습니다.

Closure

클로저는 JavaScript가 가지는 중요한 특성 중 하나입니다.

외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를
참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다.

클로저의 이해를 돕기 위해 예시 코드를 보겠습니다.

function outer() {
	const x = 10;
	const inner = function () { console.log(x); };
	return inner;
}

const innerFunc = outer();
innerfunc() // 10

outer 함수를 호출하면 outer 함수는 중첩 함수 inner를 반환하고 생명 주기를 마감합니다. 즉, outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거됩니다. 이때 outer 함수의 지역 변수x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되었으므로 outer 함수의 지역 변수 x 또한 생명 주기를 마감합니다. 따라서 outer 함수의 지역 변수 x는 더이상 유효하지 않게 되어 변수 x에 접근할 수 있는 방법은 없어보입니다.

그러나 위 코드를 실행하면 outer 함수의 지역 변수의 x의 값인 10이 출력됩니다.

useState

useState 메소드는 클로저를 이용해서 함수의 상태를 기억을 하게됩니다.

const useState = (initValue) => {
    let value = initValue

    const getValue = () => value

    const setValue = (changeValue) => {
        value = changeValue
    }

    return [getValue, setValue]
}

const [value, setValue] = useState(0)

console.log(value()) // 0
setValue(value()+1)
console.log(value()) // 1

클로저 개념을 이용해서 구현해본 useState입니다. 정확하진 않지만 개념적으로 이해하시면 될 것 같습니다. useState 함수를 실행하게되면 파라미터로 넘어온 InitValue값을 지역변수인 value에 할당하게되고 getValue를 호출하면 내부 value값을 리턴하고 setValue를 호출하면 새로운 파라미터로 넘어온 changeValue값을 지역변수에 할당하게 됩니다.

실제 getValue, setValue를 사용하는 시점은 useState 호출이 끝난 후지만, 클로저가 value값을 기억하고 있기때문에 그 이후에도 접근이 가능합니다.

이 메소드가 좀 더 정확하게 작동하기 위해서는 getValue에 접근할 때 메소드가 아닌 변수로 접근할 수 있어야합니다.

const useState = (initValue) => {
    let value = initValue

    const setValue = (changeValue) => {
        value = changeValue
    }

    return [value, setValue]
}

const [value, setValue] = useState(0)
console.log(value) // 0
setValue(value+1)
console.log(value) // 0

value는 변수 값이기 때문에 useState의 호출이 끝나면 그대로 리턴되어 더이상 변경할 수 없는 상태가 됩니다.

따라서 setValue를 통해서 값을 업데이트하여도 value 변수의 값을 참조할 수 없습니다.

react에서는 이 문제를 해결하기 위해 state 값을 useState메소드 외부에 배열 형식으로 저장하는 방법을 사용합니다.

리액트는 useState를 이용하여 생성한 state에 접근하고 유지하기 위해 useState메소드 바깥에 state를 저장합니다. 이 state들은 선언된 컴포넌트를 유일하게 구분할 수 있는 키로 접근할 수 있으며 배열 형식으로 저장됩니다.

useState함수 안에 선언되는 state들은 이 배열에 순서대로 저장되며, 상태가 업데이트 되었을 때, 이 상태들은 리액트 컴포넌트 바깥에 선언되어 있는 변수들이기 때문에 업데이트 한 후에도 이 변수에도 접근할 수 있게됩니다.

선언한 상태들이 컴포넌트를 키로 하는 배열에 순서대로 저장되기 때문에 hooks를 조건문이나 반복문에서 사용하게 된다면 저장되었던 순서와 맞지 않게 되어 엉뚱한 상태를 참조할 수 있습니다.

아래는 위의 개념들을 이용하여 구현해본 useState의 개념모델입니다.

let state = [];
let setters = [];
let cursor = 0;
let firstrun = true;

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

    return create
}

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

    const getValue = state[cursor]
    const setValue = setters[cursor]
    cursor++
    return [getValue, setValue]
}

Jotai

Jotai는 Recoil에서 영감을 받은 원자 모델을 사용하여 React 상태 관리에 대한 상향식 접근 방식을 취합니다.
원자를 결합하여 상태를 구축할 수 있으며 원자 종속성을 기반으로 렌더링이 최적화됩니다. 
이는 React 컨텍스트의 추가 리렌더링 문제를 해결하고 메모이제이션 기술의 필요성을 제거합니다.

위의 클로저와 useState의 개념을 이용하여 jotai를 단순화하여 구현을 해보도록 하겠습니다.

// atom의 상태 변화 추적
const atomStateMap = new WeakMap()

// 초기 값을 포함하는 객체 반환
const atom = (initValue) => ({ init: initValue })

const createAtom = (state) => {
    const atomState = atom(state)
    const value = { value: atom.init, listeners: new Set()}
    atomStateMap.set(atomState, value)

    return atomState
}

const getAtomState = (atom) => atomStateMap.get(atom)

const useAtom = (atom) => {
    const atomState = getAtomState(atom)
    
    const [value, setValue] = useState(atomState.value);
    
    useEffect(() => {
        const callback = () => setValue(atomState.value)
        
        // 동일한 아톰을 여러 컴포넌트에서 사용할 수 있도록 리스너 부착
        atomState.listeners.add(callback);
        callback();
        return () => atomState.listeners.delete(callback)
    }, [atomState])
    
    const setAtom = (changeValue) => {
        atomState.value = changeValue;
        // 구독한 컴포넌트에게 변경을 알림
        atomState.listeners.forEach((x) => x());
    };
    
    return [value, setAtom]
}

위의 코드로 구현한 jotai는 완전한 예가 아니라 단순화하게 구현이 되었습니다.

추가로 reset기능을 구현을 해본다면 다음과 같이 할 수 있습니다.

// atom의 상태 변화 추적
const atomStateMap = new WeakMap()

// 초기 값을 포함하는 객체 반환
const atom = (initValue) => ({ init: initValue })

const createAtom = (state) => {
    const atomState = atom(state)
    const value = { value: atom.init, init: atom.init, listeners: new Set()}
    atomStateMap.set(atomState, value)

    return atomState
}

const getAtomState = (atom) => atomStateMap.get(atom)

const useAtom = (atom) => {
    const atomState = getAtomState(atom)

    const [value, setValue] = useState(atomState.value);

    useEffect(() => {
        const callback = () => setValue(atomState.value)

        // 동일한 아톰을 여러 컴포넌트에서 사용할 수 있도록 리스너 부착
        atomState.listeners.add(callback);
        callback();
        return () => atomState.listeners.delete(callback)
    }, [atomState])

    const setAtom = (changeValue) => {
        atomState.value = changeValue;
        // 구독한 컴포넌트에게 변경을 알림
        atomState.listeners.forEach((x) => x());
    };

    const reset = () => {
        atomState.value = atomState.init
        atomState.listeners.forEach((x) => x());
    }

    return [value, setAtom, reset]
}

참조

profile
풀스택 개발자를 지향하는 개발자

0개의 댓글