LAB

useState


오늘의 목표

  • 아래와 같이 useState를 이용해서 아주 익숙한 모습의 애플리케이션 작성하기!
app.tsx
1 | const App = () => {
2 | const [name, setName] = useState('홍길동');
3 | return (
4 | <div draggable>
5 | <h2>안녕하세요 {name} 님!</h2>
6 | ...
7 | <input
8 | type="text"
9 | value={name}
10 | onchange={(e) => setName(e.target.value)}
11 | />
12 |

useState 생각해보기

  • 우리는 흔히 useState를 설명할때, [state,setState]를 반환하고, initialState를 인자로 받는 함수라고 설명합니다.
  • 그럼 일단 위에서 말한 모양의 함수를 작성해보겠습니다.
core/react.js
1 | const React = {
2 | ...
3 | useState: (initialState) => {
4 | let state = initialState; //초기로 받은 값을 state에 할당
5 | function setState(newState) {
6 | state = newState;
7 | }
8 |
9 | //state,setState를 반환한다
10 | return [state, setState];
11 | },
12 | };
  • useState는 React.useState로 호출하기 때문에, React객체에 추가해주었습니다.
  • 인자로 받은 initialState를 지역변수인 state에 할당해주고, 재할당이 일어나는 setState라는 내부 함수를 같이 return 했습니다.
small-react-image1
  • 위의 코드에는 작성하지 않았지만, 호출 흐름을 알기 위해console.log()를 작성해 두었습니다.
  • 브라우져 콘솔을 확인해보면, useState와 setState가 인자와 함께 잘 호출되는 것을 확인할 수 있습니다.
  • state값이 변하면 렌더링이 일어나야 합니다. 일단 단순히 setState가 일어나면 재렌더링이 일어날수 있게 아래와 같이 rerender라는 함수를 만들고, setState함수 실행 시에 해당 함수가 동작하도록 해보겠습니다.
core/reRender.js
1 | export function reRender() {
2 | console.log("리렌더링 일어남");
3 | const root = document.getElementById("app");
4 | root.innerHTML = "";
5 | render(React.createElement(App, null), root);
6 | }
7 |
image1
  • 콘솔을 보니 newState와 함께 setState 호출되면서 리렌더링이 일어나긴했는데, 다시 initialState으로 내부state가 덮어씌워져 버렸습니다.

  • 전역에서 상태관리 하기

    • 위에서 setState가 호출될때마다 상태가 초기화 되는 문제를 확인했습니다.
    • setState가 호출될때마다 상태가 초기화 되는 이유는, state 변수는 useState라는 함수의 지역변수이기 때문에, 리렌더링이 일어나 재실행 될때마다 initialState를 할당 받기 때문입니다.
    • 또한 단순히 전역 변수로 해결되는것 보다는, 이 state가 수정되었나? 안되었나? 를 판단해서 수정되었을때는 수정된 state가 유지되어야 할것 입니다.
    core/react.js
    1 | ...
    2 | let state;
    3 |
    4 | const React = {
    5 | createElement:...,
    6 | useState: (initialState) => {
    7 | // state를 initialState로 설정하기 전에 확인
    8 | state = state || initialState;
    9 |
    10 | function setState(newState) {
    11 | state = newState;
    12 | rerender(); //값이 바뀌면 리렌더링 실행
    13 | }
    14 | ...
    15 |
    image1
    브라우져 콘솔을 확인해보면, setState가 일어나 리렌더링시에도 initialState로 덮어씌워지지 않는것을 확인할 수 있습니다.

    여러개의 state 관리하기

    • 그런데 지금 상태는 state를 전역으로 관리하고 있기때문에, useState를 여러번 호출하면, 마지막 setState로 인해 변경된 state값이 모든 useState로 선언된 state에 할당될 것 같습니다
    • 저는 항상 문제가 될것 같더라도 냅다 코드를 작성하고, 그 이후에 코드를 살펴보며 생각후에 문제 해결을 하는 편이라, 일단 여러번 useState를 호출하는 코드를 작성해보겠습니다.
    app.tsx
    1 | ...
    2 | return (
    3 | <div>
    4 | <h2>안녕하세요!{name}님</h2>
    5 | <p>저는 grimza99 입니다.</p>
    6 | <input type="text" onchange={(e) => setName(e.target.value)} />
    7 | <h2>오늘 기분은 어떠신가요?</h2>
    8 | <p>{todayMood}</p>
    9 | <button onclick={() => {setTodayMood("좋아요")}}>좋아요</button>
    10 | <button onclick={() => {setTodayMood("우울해요")}}>우울해요</button>
    11 | </div>
    12 | );
    image1image1
    • 기분이 홍길동인 사람이 되어버렸습니다 ;;
    • 위와 같이 두가지 useState에서 전역스코프인 state가 공유 되고 있는것을 확인할 수 있습니다.
    • 제 생각에 문제의 원인은 전역으로 관리되는 state변수가 하나이기 때문인것 같습니다.
    • 전역으로 관리할 state를 배열형태로 바꾸고, 각 useState에서는 전역 변수인 stateArr의 인덱스를 지역 스코프로 가지게 하고, state참조와 setState를 일으키면 만사 해결 될것 같습니다
    core/react.js
    1 |
    2 | let stateArr = [];
    3 | let stateIndex = 0;
    4 |
    5 | const React = {
    6 | ...,
    7 | useState: (initialState) => {
    8 | const idx = stateIndex;
    9 | stateArr[idx] = stateArr[idx] || initialState;
    10 |
    11 | function setState(newState) {
    12 | stateArr[stateIndex] = newState;
    13 | rerender(); //값이 바뀌면 리렌더링 실행
    14 | }
    15 |
    16 | stateIndex++; //다음 useState를 위해 인덱스 증가
    17 |
    18 | return [stateArr[idx], setState];
    19 | }
    20 | };
    image1
    • 위와 같이 코드를 수정해주고, 브라우져 콘솔을 확인해보면, 초기 로딩시 각 useState가 고유한 인덱스를 가지고, 참조하고 있습니다.
    image1
  • 그리고 name state를 "유선향" 으로 변경해보니, stateArr에 새로운 값이 등록되고, 바뀐 idx에 의해 name state가 참조하고 있는걸 볼 수 있습니다.

  • state가 항상 같은 idx를 참조하게 하기

    • 지금 위의 모습을 보면, state가 변동 될때마다 배열에 새로운 요소가 추가 되고 있습니다.
    • 배열에 말도 안되게 많은 요소가 추가되는것을 냅둘수는 없으니, useState 에서는 항상 같은 요소를 참조하게 만들어야 할것 같습니다.
    • 예를들어, 지금 저는 프로젝트에 state 두개를 관리하고 있는데,(name, todayMood) 전역 변수로 선언된stateArr가 관리하고 있는 state갯수만큼, 즉 딱 2개의 요소만 가지게 하고, 해당useState는 자신의 인덱스에 해당하는 요소만 참조, 변경 하도록 하는것이 맞는 것 같습니다.
    core/react.js
    1 |
    2 | let stateArr = [];
    3 | let stateIndex = 0;
    4 |
    5 | const React = {
    6 | createElement: ...,
    7 |
    8 | useState: (initialState) => {
    9 | const idx = stateIndex;
    10 | stateArr[idx] = stateArr[idx] || initialState;
    11 |
    12 | function setState(newState) {
    13 | stateArr[idx] = newState; //지역변수로 인덱스 참조
    14 | stateIndex = 0; //리렌더링 전에 인덱스 초기화
    15 | rerender(); //값이 바뀌면 리렌더링 실행
    16 | }
    17 |
    18 | stateIndex++; //다음 useState를 위해 인덱스 증가
    19 |
    20 | return [stateArr[idx], setState];
    21 | },
    22 | };
    위와 같이 코드를 수정해줬습니다.
    • 처음에 useState가 호출될때, 지역변수 idx에 stateIndex를 할당해주었습니다. setState에서는 지역변수인 idx를 참조 (클로져) 하여 해당 요소만 변경되도록 했습니다.
    • 그리고 setState로 값이 변경 되면, 리렌더링시 순서대로 stateIndex를 지역변수 idx에 할당하기 위해 stateIndex를 0으로 초기화 해주도록 했습니다.
    • 아래 사진을 보면 리렌더링된 이후에 각 useState가 올바른 인덱스를 참조하는것을 확인할 수 있습니다.
    image1

    useState가 잘동작하기 위한 전제조건


    위의 코드에는 불가피한 전제조건이 있습니다. 바로
    전역 변수인 stateArr 배열은 항상 같은 순서로 참조되어야 한다라는것 입니다.
    • 제가 작성한 코드를 예시로 들면, 첫번째 useState (name state)는 항상stateArr[0]을, 두번째 useState (todayMood stae)는 항상stateArr[1]을 참조해야 합니다.
    • 만약 useState의 호출 순서가 바뀌거나, 조건문등에 의해 호출이 건너뛰어지면, 의도치 않은 state가 참조 될 수 있습니다.
    • 이것이 바로 React의 훅 규칙 중 하나인"훅은 컴포넌트의 최상위에서만 호출되어야 한다"라는 규칙입니다.
    app.tsx
    1 | ...
    2 | export const App = () => {
    3 | const [name, setName] = React.useState("홍길동");
    4 | const [isRuleBroken, setIsRuleBroken] = React.useState(false);
    5 |
    6 | if (isRuleBroken) {
    7 | const [] = React.useState("끼어들기");
    8 | }
    9 | const [todayMood, setTodayMood] = React.useState("보통");
    10 | return(...);
    11 |
    image1
    • 저는 리액트훅 규칙에서 벗어나, 조건문안에 useState를 선언하는 코드를 작성했습니다.
    • 어떤 버튼을 누르면 isRuleBroken 상태가 true가 되고, 재렌더링시 if 문안에 useState가 같이 호출되면서, 기존의 useState들의 인덱스가 밀리게 되고, "끼어들기" 라는 상태값은 stateArr의 요소로 추가되지 않았습니다.

    목표 완수!

    • 이렇게 해서, useState 훅을 아주 간단한 형태로 구현해보았습니다.
    • 이번 실습을 통해, 왜 useState 훅이 리액트 훅 규칙을 따라야 하는지 알 수 있었고,
    • useState를 왜 클로져 함수라고 하는지, 어떻게 state를 관리하는지도 대략적으로나마 알 수 있었습니다.
    아래는 위의 과정을 거쳐 완성된 index.html 파일입니다!
    <iframe/>을 이용해서 삽입 되어있습니다!