python으로 구현해본 useState, useEffect, 전역 상태관리

이한형·2022년 10월 29일
1

저번 포스팅에서 Closure와 상태관리 Hooks에서 useState, jotai를 JavaScript를 통해서 간단하게 구현을 해보았습니다.

이번 포스팅에서는 좀 더 내부의 원리를 이해를 해보고자 python으로 useState, useEffect를 구현해보았는데요. 정확하진 않지만 제가 이해하고 생각한 작동원리를 기반으로 구현을 해봤습니다. React환경과 Python환경의 차이가 있기때문에 로직정도만 보시면 될 것 같습니다.

useState

import sys

class State:
    def __init__(self):
        self.states = []
        self.setters = []
        self.caller = []
        self.cur = 0

    def createSetter(self, cur):
        def setter(newValue):
            nonlocal self
            self.states[cur] = newValue

        return setter

    def createState(self, initValue, caller):
        self.states.append(initValue)
        self.setters.append(self.createSetter(self.cur))
        self.caller.append(caller)
        self.cur += 1
        return self.states[self.cur - 1],self.setters[self.cur - 1]

store = State()

class를 통하여 state의 모델을 구현하였습니다.
상태값을 저장하는 states배열, 상태값은 변경하는 함수를 저장하는 setters배열, 호출한 함수를 기억하는 caller배열로 이루어져있습니다.

createState를 통해 클래스안의 배열들에 값을 추가를 하고 createSetter함수를 통해 setter를 만들고 값을 추가 한 뒤 해당 상태와, setter를 반환합니다.

상태 값은 바깥에서 관리를 하기 위해 store를 전역 스코프에서 선언을했습니다.

def useState(initValue):
    caller = sys._getframe(1).f_code.co_name
    if not caller in store.caller:
        cur = store.cur
        state, setState = store.createState(initValue, caller)
    else:
        cur = store.caller.index(caller)
        state, setState = store.states[cur], store.setters[cur]

    def getState():
        return store.states[cur]

    return [state, setState, getState]

useState는 함수로 위 코드와 같이 구현을 했습니다.
caller를 기반으로 작동을 하게 됩니다.
caller는 자신을 호출한 함수의 이름입니다.
store 안에 caller가 존재한다면 새로운 state를 생성하지 않고 해당 caller의 state와 setter를 가져옵니다.

caller가 store안에 없다면 새로운 state를 생성하게 됩니다.

그리고 python closure를 통해 값을 반환하는 getState함수 또한 선언을 해봤습니다.

마지막으로 state, setter, getState값을 반환을 하게 됩니다.

python함수 안에서는 state값을 변경을 한 후 값이 변경이 되었는지 바로 알아보기 위해서는 closure를 통해서 접근해야만 정확하게 정보를 얻을 수 있기 때문에 저렇게 3가지 값을 반환을 하였습니다.

좀 더 정확한 이해를 위해 아래의 코드를 살펴보도록 하겠습니다.

[value, setValue, getValue] = useState(0)
print(value) // 0
print(getValue()) // 0
setValue(1)
print(value) // 0
print(getValue()) // 1

업데이트 한 값은 getValue를 통해서 바로 확인을 할 수 있습니다.
그러면 state값은 어떻게 사용을 하느냐?

def test():
    [value, setValue, getValue] = useState(0)
    print(value)
    setValue(value+1)

for i in range(1000):
    test()

결과는 아래와 같습니다.

0
1
2
...
999

제가 작성한 useState는 caller를 기반으로 작동하기 때문에 같은 함수가 여러번 실행된다면 useState는 새로운 값을 만들어 내지 않고 이전의 값을 기반으로 state를 반환하게 됩니다.

React에서는 상태변화가 일어나면 render()가 실행되기 때문에 state값만을 반환하여도 문제가 없지만 Python에서는 그렇게 작동이 하지 않기 때문에 state와 getState를 반환하였습니다.

useEffect

useEffect는 의존 배열이 변경이 되었을때 전달받은 function을 실행시키게 하는 역할을합니다.

class Effect:
    def __init__(self):
        self.caller = []
        self.funcs = []
        self.dependency = []
        self.cur = 0

    def createEffect(self, func, dependency, caller):
        self.caller.append(caller)
        self.dependency.append(dependency)
        self.funcs.append(func)
        self.cur += 1

    def changeDependency(self, dependency, cur):
        self.dependency[cur] = dependency

    def checkDependency(self, dependency, cur):
        for old, new in zip(self.dependency[cur], dependency):
            if old != new:
                self.changeDependency(dependency, cur)
                return True
        return False
        
store = Effect()

class를 통하여 Effect모델을 구현하였습니다.
호출한 함수를 기억하는 caller배열, 실행시킬 함수를 저장하는 funcs배열, 의존배열들을 저장하는 dependency배열로 이루어져있습니다.

createEffect를 통해 caller, dependency, funcs배열들에 값을 저장하게 됩니다.

checkDependency를 이용하여 기존의 의존배열과 현재의 의존배열을 비교하여 변경되었다면 저장되어 있던 기존의 의존배열을 새로운 의존배열로 변경하고 의존배열이 변경되었다고 알리기 위해 True를 반환하게 됩니다.

chagneDependency는 의존배열이 변경되었을 경우 기존의 의존배열을 새로운 의존배열로 변경을 시키게됩니다.

effect를 관리하는 store또한 바깥에서 관리를 하기 위해 전역스코프에서 선언을 하였습니다.

def useEffect(func, dependency=None):
    caller = sys._getframe(1).f_code.co_name
    if not caller in store.caller:
        store.createEffect(func, dependency, caller)
        func()
    elif caller in store.caller:
        cur = store.caller.index(caller)
        if store.checkDependency(dependency, cur):
            func()
    elif not dependency:
        func()

useEffect는 함수로 위 코드와 같이 구현을 했습니다.
useEffect 또한 caller를 기반으로 작동을 하게 됩니다.

최대한 React의 useEffect와 동일하게 작동을 하게하기 위하여 dependency의 default value를 None으로 설정을 하도록 했습니다.

caller가 store의 caller배열 안에 없다면 새로운 Effect를 생성하고 전달받은 함수를 실행하게 됩니다.

의존배열이 없다면 함수가 호출될 때마다 전달받은 함수를 실행하게 됩니다.

그리고 매번 useEffect가 호출이 될 때마다 호출한 함수의 Effect를 기반으로 의존배열이 변경이 되었는지 검사를 하게 되고, 변경이 되었다면 전달받은 함수를 실행하게 됩니다.

def test():
    def message():
        print('실행되었음')
    useEffect(message)

for i in range(3):
    test()

위 코드와 같이 의존배열을 넣지 않고 실행을 하게되면 다음과 같이 결과가 나오게 됩니다.

실행되었음
실행되었음
실행되었음

의존배열에 빈 값을 리스트를 넣게되면 다음과 같이 한 번만 실행을 하게 됩니다.

def test():
    def message():
        print('실행되었음')
    useEffect(message, [])

for i in range(3):
    test()
실행되었음

의존 배열을 넣고 다음과 같이 실행을 하면 어떻게 될까요?

def test(nums):
    [value, setValue, getValue] = useState(0)
    if nums == 1:
        setValue(2)
    def message():
        print('실행되었음')
    useEffect(message, [value])

for i in range(3):
    test(i)

useEffect가 처음 한 번 실행이되고 value가 변경이 될 때 한번 실행이 됩니다.

실행되었음
실행되었음

전역 상태관리

간단하게 전역 상태관리를 할 수 있는 기능을 구현을 해보았습니다.
hash를 이용하여 간단하게 구현을 하였고 react의 상태관리 라이브러리인 jotai를 기반으로 비슷하게 동작할 수 있게끔 구현을 해보았습니다.

store = {}

def atom(initValue):
    return { 'init': initValue, 'value': initValue }

def createAtom(state):
    atomState = atom(state)
    atomId = id(atomState)
    store[atomId] = atomState
    return atomState
    
def useAtom(state):
    def getValue():
        return store[id(state)]['value']

    def setValue(value):
        store[id(state)]['value'] = value

    return [store[id(state)]['value'], setValue, getValue]

store를 바깥에서 관리를 하기 위해 동일하게 전역 스코프에서 선언을 하였습니다.

초기 값을 전달받아 atom을 생성하는 atom함수가 있습니다.

createAtom에 초기값을 전달하여 메모리 id를 기준으로 생성한 atom을 store에 저장을 하고 atom을 반환합니다.

useAtom을 통하여 atom의 state와 상태를 변경할 수 있는 setter, 그리고 클로저를 통하여 접근할 수 있는 getValue함수를 반환하게 됩니다.

간단한 예제를 살펴보도록 하겠습니다.

counterAtom = createAtom(0)

def counter():
    [value, setValue, getValue] = useAtom(counterAtom)
    setValue(value+1)


def check():
    [value, setValue, getValue] = useAtom(counterAtom)

    def message():
        print(f'{value} 변경감지')
    useEffect(message, [value])

for i in range(10):
    counter()
    check()

실행 결과는 다음과 같습니다.

1 변경감지
2 변경감지
3 변경감지
4 변경감지
5 변경감지
6 변경감지
7 변경감지
8 변경감지
9 변경감지
10 변경감지

useState같은 경우는 해당 함수 안에서만 생존을 해야하는 값에 대해서 사용을 하였지만,
useAtom같은 경우는 해당 함수 뿐만이 아니라 여러 곳에서 상태에 대한 접근을 해야할 때 유용하게 사용을 할 수 있습니다.

결론

react의 구조를 좀 더 이해해보고 구현을 해보기 위해 python으로 구현을 해봤는데 생각보다 그렇게 어렵지는 않게 구현을 할 수 있었던 것 같습니다.
python에서 react의 메서드를 사용할 일은 거의 없겠지만 한번씩은 재밌게 이용해볼 수 있을 것 같습니다.

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

0개의 댓글