jh.nrtv

[React] Vanilla JS로 React의 useState hook 구현하기 본문

React

[React] Vanilla JS로 React의 useState hook 구현하기

wlgus3 2023. 12. 20. 01:49

 

✅ 들어가며

React를 더 심도있게 공부하기 위한 방법을 고민하다보니 직접 React의 여러 hook을 구현해 보는 것이 효과적일 것이라는 생각이 들었다. 

먼저 리액트의 훅 중에서 가장 유명하고 특징적인 useState를 구현해보고자 한다.

 

사실 useState의 구조와 사용법은 간단하기 때문에 쉽게 가능할것이라 생각했으나 큰 오산이었다... ㅠ

라이브러리나 프레임워크가 아닌 html와 js만을 사용한 프로그래밍은 익숙하지 않기도 하거니와,

event listener가 중복해서 추가된다던가 의도치 않게 재실행 된다던가 하는 수 많은 시행착오가 있었기에 시간도 매우매우 많이 걸렸다.

 

그래도 감사하게도 먼저 구현하시고 그 과정을 공유하신 분( 개발자 황준일 님 )들이 많이 계시기에 해당 자료들을 참고하고, 공식 문서도 읽어보면서 간단하게나마 구현하기 위해서 노력했다.

 

 

🔸 Hook과 useState

 

먼저 Hook은 React 16.8에 새로 추가된 기능으로, class를 작성하지 않고도 state와 다른 React의 기능들을 편리하게 사용할 수 있도록 해준다. React docs에서 정의하는 Hook의 의미는 다음과 같다.

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. Hook은 class 안에서는 동작하지 않습니다. 대신 class 없이 React를 사용할 수 있게 해주는 것입니다.
- React docs -

 

 

리액트의 단방향 바인딩 패턴을 가지고 있기에 여러 컴포턴트에서 같은 state를 공유한다면 가장 가까운 조상으로 State 끌어올리기를 통해 데이터의 '진리의 원천(source of truth)'이 하나만 존재하도록 해야한다. 

 

useState는 앞서 설명한 리액트의 단방향 바인딩으로 인한 상태관리의 불편함을 더 편리하게 관리하고 구현할 수 있도록 한다.

 

 

나는 useState를 이용해서 간단한 Counter를 구현해 보았다.

 

✅ 구현

크게 구현해야 할 요소는 네 가지다.

  • useState : 초기값을 받아 state, setState를 뱉어내고 state가 변경될 때마다 해당 파트 재랜더링
  • stateComponent : useState를 적용할 컴포넌트
  • render : 랜더링 실행함수
  • increaseCount : 클릭을 감지하고 setState를 실행해서 state를 증가시킬 함수

 

🔸 시도1 

 

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>React Hooks with Vanilla JS</title>
    </head>
    <body>
        <div>
            <h1>Vailla JS useState Counter</h1>
            <h3 id="statedComponent"></h3>
            <button id="increase-button">+</button>
        </div>

        <script>

			// useState
            let _state; //전역 state
            const useState = (initState) => {
                const state = _state !== undefined ? _state : initState;

                const setState = (newState) => {
                    _state = newState;
                    render();
                };

                return [state, setState];
            };

            // stateComponent
            const stateComponent = () => {
                const [count, setCount] = useState(_state !== undefined ? _state : 0); //useState 실행1
                console.log("랜더링", _state, count);
                return `<div>
                    count: <span id="counterState">${count}</span>
                    </div>`;
            };

            // render
            const render = () => {
                const $stateId = document.getElementById("statedComponent");
                $stateId.innerHTML = /*html*/ stateComponent();
            };

            //increaseCount 선언 및 이벤트 리스터 할당
            const increaseCount = () => {
                const [count, setCount] = useState(_state !== undefined ? _state : 0); //useState 실행2
                setCount(count + 1);
            };

            const setIncrease = () => {
                const increase = document.getElementById("increase-button");
                increase.removeEventListener("click", increaseCount); // 기존 이벤트 리스너 제거
                increase.addEventListener("click", increaseCount); // 새로운 이벤트 리스너 추가
            };
            setIncrease();

            render();
        </script>
    </body>
</html>

 

위 코드도 정상적으로 작동하는 듯 보이나, 문제점이 있어 보였다.

1. useState가 두 번 실행되어야 정상적으로 작동한다. -> 진정한 useState라 할 수 없음 

2. state는 _state로 전역에서 사용 가능하나, setState는 컴포넌트 외부에서 사용할 수 없다. 

 

이 두 문제점은 연결되어

 

 

🔸 시도2

위 코드를 아래와 같이 개선했다. 

 

먼저 전역으로 _state, _useState를 선언하고, useState 함수가 전역 함수들에 값과 set 함수를 넣도록 했다. 

이렇게 함으로써 컴포넌트 내에서 useState를 한 번만 실행해도 컴포넌트 외부에서 해당 state와 set 함수를 참조해서 사용할 수 있다. 

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>React Hooks with Vanilla JS</title>
    </head>
    <body>
        <div>
            <h1>Vailla JS useState Counter</h1>
            <h3 id="stateComponent"></h3>
            <button id="increase-button">+</button>
        </div>

        <script>

            // useState
            let _state;
            let _setState;

            const useState = (initState) => {
                _state = _state !== undefined ? _state : initState;

                const setState = (newState) => {
                    _state = newState;
                    render();
                };

                _setState = setState;

                return [_state, _setState];
            };

            // stateComponent 컴포넌트
            const stateComponent = () => {
                const [count, setCount] = useState(0);
                console.log("렌더링", _state, count);
                return `<div>
                    count: <span id="counterState">${count}</span>
                </div>`;
            };

            // 랜더링
            const render = () => {
                const $stateComponent = document.getElementById("stateComponent");
                $stateComponent.innerHTML = stateComponent();
            };

            // increaseCount 
            const increaseCount = () => {
                const count = _state !== undefined ? _state : 0;
                _setState(count + 1);
            };

            // increaseCount 이벤트 리스너 할당
            const increaseButton = document.getElementById("increase-button");
            increaseButton.addEventListener("click", increaseCount);

            // 초기 랜더링
            render();
        </script>
    </body>
</html>

 

 

다만 이 useState도 온전한 구현이라고 볼 수 없다.... ㅠㅠ

그 이유는 원래 useState Hook은 상태의 중복 선언이 가능해야 한다.

 

위 코드는 useState가 중복 선언되면 전역 state, setState가 재할당되어 기존 useState는 무시되는 현상이 발생할 것이다 ...ㅎㅎ ... 

 

지금 당장 고쳐보고 싶지만 이미 예상보다 너무 긴 시간을 썼기에 ... 다음번에 시도하는 것으로 하겠다.

 

 


 

 

참고 

 

Vanilla Javascript로 웹 컴포넌트 만들기 | 개발자 황준일

Vanilla Javascript로 웹 컴포넌트 만들기 9월에 넥스트 스텝open in new window에서 진행하는 블랙커피 스터디open in new window에 참여했다. 이 포스트는 스터디 기간동안 계속 고민하며 만들었던 컴포넌트

junilhwang.github.io

 

 

 

 

 

 

 

'React' 카테고리의 다른 글

[React] - Virtual Dom, React Diffing Algorithm  (0) 2023.01.25
React - 번들링과 웹팩  (0) 2023.01.18
Redux -React 상태관리  (0) 2022.12.27
React- Custom Component- CDD, CSS in JS  (0) 2022.12.22