useState
오늘의 목표
- 아래와 같이
useState를 이용해서 아주 익숙한 모습의 애플리케이션 작성하기!
app.tsx
1 |const App = () => {2 |const [name, setName] = useState('홍길동');3 |return (4 |<div draggable>5 |<h2>안녕하세요 {name} 님!</h2>6 |...7 |<input8 |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 했습니다.

- 위의 코드에는 작성하지 않았지만, 호출 흐름을 알기 위해
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 |

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 |

브라우져 콘솔을 확인해보면,
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 |);


- 기분이 홍길동인 사람이 되어버렸습니다 ;;
- 위와 같이 두가지 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 |};

- 위와 같이 코드를 수정해주고, 브라우져 콘솔을 확인해보면, 초기 로딩시 각 useState가 고유한 인덱스를 가지고, 참조하고 있습니다.

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가 올바른 인덱스를 참조하는것을 확인할 수 있습니다.

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 |

- 저는 리액트훅 규칙에서 벗어나, 조건문안에 useState를 선언하는 코드를 작성했습니다.
- 어떤 버튼을 누르면 isRuleBroken 상태가 true가 되고, 재렌더링시 if 문안에 useState가 같이 호출되면서, 기존의 useState들의 인덱스가 밀리게 되고, "끼어들기" 라는 상태값은
stateArr의 요소로 추가되지 않았습니다.
목표 완수!
- 이렇게 해서, useState 훅을 아주 간단한 형태로 구현해보았습니다.
- 이번 실습을 통해, 왜 useState 훅이 리액트 훅 규칙을 따라야 하는지 알 수 있었고,
- useState를 왜 클로져 함수라고 하는지, 어떻게 state를 관리하는지도 대략적으로나마 알 수 있었습니다.
index.html 파일입니다!<iframe/>을 이용해서 삽입 되어있습니다!