useState는 함수형 컴포넌트 안에서 state를 사용할 수 있게 해주는 기본 Hook입니다. useState에서는 주요 키워드인 state와 setState가 무엇인지 간단하게 살펴보겠습니다.
2.1.1 state
React에서의 state는 데이터가 관리 및 저장되는 공간을 의미합니다. React에서는 state가 어떻게 쓰이는지, state 사용 예제를 통해 간단하게 살펴보겠습니다.
위의 코드는 함수형 컴포넌트와 클래스형 컴포넌트 안에서 state를 사용한 예시입니다. 함수형 컴포넌트에서는 useState Hook을 사용하여 첫 번째 인자인 변수 price에 상태 값으로 1000인 초깃값을 넣어주었습니다.
반면, 클래스형 컴포넌트는 this.state = { price: 1000 }; 을 사용하여 price를 1000으로 초기화해주었습니다. 클래스형 컴포넌트에서는 반드시 constructor(생성자) 안에서 this를 사용하여 값을 초기화해주어야 합니다.
state는 JavaScript의 객체이며, 컴포넌트 안에서 관리됩니다. 컴포넌트는 자신의 state를 props를 통해 자식 컴포넌트에 전달하고 영향을 미칠 수 있습니다. state를 가진 컴포넌트 외에 다른 컴포넌트는 접근할 수 없기 때문에, 로컬 또는 캡슐화라고 표현되기도 합니다.
💡
props는 무엇인가요?
props도 속성을 나타내는 데이터이자, JavaScript의 객체입니다. 컴포넌트 안에서 관리되는 state와 다르게 ‘컴포넌트에 전달한다’는 방식의 차이가 있습니다.
React 컴포넌트들은 자신의 props를 자체적으로 수정해서는 안 되며, 입력값에 대한 결과가 변함없이 동일한 순수 함수처럼 동작해야 합니다.
아래 렌더링 예시를 통해 props를 살펴보겠습니다.
props로 {color: “초록”} 단일 객체를 Introduce 컴포넌트에 호출되어 전달받고, 화면에서는 Introduce 컴포넌트가 반환한 값, 나는 초록색을 좋아해! 가 나타납니다.
즉, 해당 컴포넌트에 단일 객체로 전달하는데 바로 이 ‘단일 객체’가 props입니다.
2.1.2 setState
위에서 state는 컴포넌트 안에서 데이터가 관리되고 저장되는 공간임을 알아보았습니다. state의 값을 갱신하기 위해서 함수형 컴포넌트와 클래스형 컴포넌트에서는 setState 함수를 이용합니다. 아래 코드는 클래스형 컴포넌트에서 setState를 사용한 예시 코드이며, 함수형 컴포넌트에서의 사용 방법은 2.3.3 챕터에서 자세히 살펴보겠습니다.
이 책에서 설명할 React의 기본 Hook인 useState는 setState 함수를 통해 새 state 값을 반환받아서 컴포넌트 리렌더링을 큐에 등록하고 최신 state로 갱신해 줍니다. 또한 props와 동일하게 setState 함수도 동일성이라는 특징을 가지고 있어, 리렌더링 시에 변경되지 않습니다.
2.1.3 useState
위의 예시들은 클래스 컴포넌트를 포함하여 설명하고 있습니다. React Hook이 등장하기 전, 함수 컴포넌트 안에서는 state 사용이 불가했으나 이제는 React 기본 Hook인 useState로 함수 컴포넌트 안에서도 state를 사용할 수 있습니다. 함수형 컴포넌트에서 사용할 수 있는 useState의 특징은 다음과 같습니다.
상태를 유지하는 값인 state 변수와 그 값을 갱신하는 setState 함수를 쌍으로 반환합니다.
useState 호출 시, 컴포넌트 state는 완전히 독립적으로 이루어져 작동됩니다.
함수형 컴포넌트에서는 state의 초깃값으로 아래 코드와 같이 객체를 포함한 배열, 문자, 숫자 타입을 가질 수 있으며, 하나의 컴포넌트 안에서 여러 개의 useState Hook을 사용할 수 있습니다.
2.2 useState를 사용하는 이유
React에서 상태를 관리할 때 일반 변수가 아닌 state 변수를 활용하는 이유는 DOM 렌더링과 밀접한 연관이 있습니다. 어떤 이유로 state 변수를 사용해야 하는지, 그중에서도 왜 useState를 사용해야 하는지 예제를 통해 함께 살펴보겠습니다.
2.2.1 일반 변수를 사용했을 때
아래 예제는 버튼을 클릭할 때마다 오늘 마신 물이 몇 잔인지 기록할 수 있습니다. 버튼의 클릭 횟수에 따라 변하는 상태를 저장할 수 있도록 일반 변수인 drink를 0으로 초기화해줍니다. 버튼을 클릭하여 drinkWater 함수를 호출하면 drink 변수에 클릭 횟수가 누적되어 ⓵에 결과를 출력해 줍니다.
그림 2-1
drink의 상태가 변경되면 화면에서도 동일하게 변경된 값이 업데이트되는 결과를 예상했을 겁니다. 하지만 콘솔에서 확인할 수 있듯이 drink의 값은 변하고 있지만 실제 화면에는 변경된 값이 렌더링 되지 않고 있습니다.
이유는 컴포넌트가 DOM에서 렌더링 될 때 생명주기 이벤트가 발생하는 것에 있습니다. 함수 컴포넌트 안에서 일반 변수는 렌더링 될 때마다 초기화 되지만, state 변수를 사용하는 경우 변경된 값을 반영하여 업데이트해 주기 때문입니다.
2.2.2 State 변수를 사용했을 때
이제 useState를 사용하여 drink의 상태를 저장해 보겠습니다. useState를 선언해 주고, setDrink 함수를 사용하여 변경된 상태를 업데이트합니다.
그림 2-2
일반 변수를 사용했을 때와 다르게 setDrink 함수로 상태 값을 갱신해 줬기 때문에 drink의 상태가 변경되면 자동으로 리렌더링 되어 최신 값이 반영되는 것을 확인할 수 있습니다.
2.2.3 React가 useState 사용을 권장하는 이유
함수 컴포넌트는 state 변경으로 리렌더링이 되면 컴포넌트 내부에 있는 변수들이 모두 초기화됩니다. 기존의 컴포넌트가 새로고침 되는 것이 아니기 때문에 state의 최신 상태를 새로운 컴포넌트로 갱신해 주는 것에 더 가깝다고 볼 수 있습니다.
useState는 상태 주기 메서드와 state를 함수 컴포넌트에서 사용할 수 있게 만들어주고, 함수의 특성을 활용하여 현재 상태 값을 가지고 있는 state 변수와 값을 변경시켜주는 setState 함수를 사용하여 배열을 반환해 줍니다.
이는 기존의 state를 복사하여 새롭게 state를 갱신해 주기 때문에 객체의 불변성을 지킬 수 있고, 발생할 수 있는 오류들을 미리 방지할 수 있습니다. 또한, setState를 통해 state에 변화가 생기면 알아서 리렌더링을 해주기 때문에 훨씬 간결하고 효율적인 상태 관리가 가능합니다.
❓
setState 함수를 사용해야 하는 이유
앞서 state는 객체라고 했습니다. 그렇기 때문에 state 값을 변경하고 싶다면 주의해야 할 점이 있습니다. state를 직접 변경하게 되는 경우, 새로운 값과 이전 값이 가리키는 주소가 같아지면서 객체의 불변성이 깨지게 되고, React 또한 참조 값이 같아 변화가 없다고 판단하여 컴포넌트를 리렌더링 해주지 않습니다. React는 state에 변화가 있다는 것을 state 객체의 참조 값(주소값)만 비교하여 판단하기 때문입니다. 그렇기 때문에 React에서는 변경된 state 값을 바로 반영해 주거나 이전 state 값을 사용하여 state를 바로 변경해 주고 싶은 경우, 객체 대신 함수를 값으로 전달하여 state 값을 변경하는 방법을 권장합니다.
이제부터 실습을 통해 useState의 사용 방법과 작동 원리를 확인해 봅시다.
2.3 useState 사용해보기
useState를 사용하기 위해 환경설정을 하도록 하겠습니다. React 설치는 터미널 창에서 다음과 같이 입력합니다.
2.3.1 import 하기
useState라는 함수를 사용하기 위해서는 react 모듈에서 useState를 불러와야 합니다.
💡
useState를 정확히 어디서 불러오는 건가요?
React를 설치하면 생성되는 폴더들 중에는 node_module이라는 폴더가 있는데, 그 안에 있는 react 모듈 폴더를 잘 들여다보면 다양한 Hooks 함수들이 선언되어 있습니다. 여기에서 Hook을 불러와 사용합니다.
그림 2-3 node_modules/react/cjs/react.development.js
2.3.2 useState 선언하기
useState 함수에 초깃값을 넣어 호출하면 두 가지 값이 반환되는데, 이는 구조 분해 할당 문법을 이용하여 각각 state와 setState라는 변수와 함수로 할당됩니다. 인자로 넘긴 초깃값은 state 변수를 초기화할 수 있습니다.
현재 상태 값은 state 변수에 저장되고, state 변수 안의 값을 변경시키고 싶다면 setState 함수를 호출할 수 있습니다. 이때 state라는 변수 명은 count, email 등으로 자유롭게 변경할 수 있습니다.
다음 코드는 count라는 변수는 0으로 초기화되어있고, setCount 함수는 count 값을 변경시킬 수 있는 함수임을 보여주고 있습니다.
2.3.3 state 가져오기, 갱신하기
위에서 생성한 변수 count를 가져와 사용해 봅시다.
그럼 이 count 값은 어떻게 변경할 수 있을까요? 위에서 설명한 바와 같이 count 값을 변경하고 싶다면 setCount라는 함수를 사용하면 됩니다.
버튼을 클릭하면 onClick 이벤트로 인해 setCount 함수를 호출하고, count 변수에 1을 더해 값을 갱신합니다. count 변수는 클릭될 때마다 1씩 증가된 값을 가지게 됩니다.
2.3.4 (실습) useState의 작동방식
그렇다면 useState를 실행했을 때 값은 어떻게 갱신되는 걸까요? 이 과정은 다음 예제를 통해 알아봅시다.
그림 2-4
버튼을 클릭할 때마다 화면이 리렌더링 되면서 count 횟수가 증가하고 있는 것을 확인할 수 있습니다. 콘솔창을 통해 컴포넌트가 렌더링 되고 count 변수의 값이 갱신되는 과정을 확인해 봅시다.
💡
React Developer Tools 확장 프로그램을 설치하면 렌더링이 되는 구간을 표시해주는 것을 확인할 수 있습니다.
이후, 👍 버튼을 클릭하면 바로 setCount 함수가 호출되고 현재 시점의 count를 출력합니다.
렌더링이 된 후, 현재 시점의 count 값을 출력합니다.
혹시 콘솔창에서 조금 의아한 부분이 보이시지 않나요?
👍 버튼을 클릭하면 setCount 함수가 호출되어 바로 count 가 갱신될 것 같지만, 실제로는 버튼을 클릭한 이후에도 count 값은 그대로입니다. 리렌더링이 되고 난 후에야 count 가 갱신되는 것을 확인할 수 있는데요, 왜 이러한 현상이 일어나는 걸까요?
그림 2-5
위 그림을 보며 업데이트 과정을 순서대로 살펴보겠습니다.
setCount 함수가 호출되면 갱신할 값을 react 모듈 내 선언된 전역 변수에 할당해 줍니다.
이후, 컴포넌트가 리렌더링되고 App.js가 다시 실행됩니다. 그럼 초깃값 0을 다시 useState에게 전달하면서 useState 함수가 호출됩니다.
호출된 useState 함수는 먼저 react 모듈 내 선언된 전역 변수에 값이 있는지 확인합니다.
값이 있다면 초깃값 0은 무시되고 전역 변수에 저장해두었던 값과 setCount 함수를 반환합니다.
위에서 useState는 [변수, 함수] 이렇게 두 가지 값을 반환한다고 했었죠?
반환받은 두 값은 구조 분해 할당을 통해 각각 count 변수와 setCount 함수에 할당됩니다.
정리하자면, setCount 함수는 현재의 state 변수를 변경시키지 않습니다. 대신 리렌더링이 되고 난 이후의 useState가 반환할 값을 변경해 주고, 컴포넌트를 리렌더링 시켜주는 역할을 하게 됩니다.
💡
useState의 작동 방식을 직접 확인해 보고 싶다면?
node_module > react > imd > react.development.js 파일에 들어가면 확인할 수 있습니다.
useState는 초깃값을 인자로 받는 함수입니다.
이 함수에는 resolveDispatcher 함수를 통해 리턴된 dispatcher의 useState 메서드에
초깃값을 전달한 결과를 반환하고 있습니다.
그림 2-6
같은 파일 내에서 resolveDispatcher 함수를 찾아보면 resolveDispatcher 함수는 또다시 ReactCurrentDispatcher 안에 있는 current라는 값을 반환하고 있습니다.
그림 2-7
마지막으로 ReactCurrentDispatcher를 같은 파일 내에서 또 타고 들어가보면, 전역으로 선언된 current라는 값을 가지고 있는 변수라고 나오는 것을 확인할 수 있습니다.
그림 2-8
2.4 (실습) useState 응용해보기
2.4.1 useState와 이벤트를 사용한 로그인 폼 만들어보기
이전 useState 사용해보기 예제에서는 버튼을 누르면 count 개수가 증가하는 간단한 예제 실습을 통해 useState의 작동 방식에 대해 알아보았습니다. 이번에는 앞에서 배운 개념을 확장하여 useState와 이벤트를 사용한 로그인 폼을 만들어 보겠습니다.
먼저 React 기본 Hook에서 제공하는 useState 함수를 사용하기 위해서는 React에서 useState를 import 받아와야 합니다. 모든 코드는 App.js에서 진행하겠습니다.
LoginForm 함수 컴포넌트를 만들어주고, 그 안에 state 변수를 선언해 줍니다.
id 와 password 라는 state 변수를 선언해 주었습니다. id 와 password의 초깃값은 처음에 id 와 password의 input 을 빈칸으로 시작하기 위해 빈 문자열로 초기화해주었습니다. 변수의 초깃값을 useState Hook의 인자로 넘겨주면 id 와 setId 두 가지 값을 배열 형태로 반환합니다.
이제 id 와 password라는 state를 사용할 수 있고, 해당 state 값을 갱신할 수 있는 setId 와 setPassword 함수를 통해 state를 변경할 수 있습니다.
💡
state 사용 시 주의할 점
state는 값을 변경할 때 state 변수에 직접 접근해서 값을 변경하는 것이 아닌, 해당 변수를 갱신할 수 있는 setState 함수를 이용해서 state 값을 바꿔주어야 합니다.
setState 함수를 이용해서 state 값을 바꿔주면 해당 컴포넌트는 state가 변경될 때마다 화면에 리렌더링 됩니다.
state 변수를 선언해 주었으니, 이제 로그인 폼을 핸들링 하는 함수를 하나 만들어주겠습니다.
handleLoginForm 함수 안에는 preventDefault() 메서드를 추가하여 form 안에 submit 역할을 하는 로그인 버튼을 눌렀을 때 창이 새로고침 되지 않도록 하였습니다.
💡
Event.preventDefault() 는 무엇일까요?
Event 인터페이스의 preventDefault() 메서드는 어떤 이벤트를 명시적으로 처리하지 않은 경우, 해당 이벤트에 대한 사용자 에이전트의 기본 동작을 실행하지 않도록 지정합니다.
id 와 password를 입력할 두 개의 input과 form을 제출할 수 있는 로그인 버튼으로 구성되도록 마크업을 작성하였습니다.
그럼 화면에서 로그인 폼이 어떻게 보이는지 한번 확인해 볼까요?
그림 2-9
실제 화면에서는 위와 같이 간단한 로그인 폼을 확인할 수 있습니다. 그러나 위 코드는 아직 완성되지 않은 코드입니다. 개발자 도구를 확인해 보면 그 이유를 친절하게 알려주고 있습니다.
그림 2-10
위에서 작성한 코드로 id 와 password의 상태는 바꿀 수 있지만, 아직 input에 값을 입력할 수는 없습니다. id 와 password의 state만 선언해 주었을 뿐, 어떤 동작을 하라고 지시하지 않았기 때문에 state 값을 변경 시켜주기 위한 이벤트를 등록해 주어야 합니다.
즉, input에 입력한 값을 React가 인지하고 동작할 수 있도록 onChange라는 이벤트를 발생시켜서 state 값을 변경시켜줘야 value 값도 변경됩니다. 그러므로 form 태그뿐 아니라, id 와 password의 각 input 태그에도 onChange라는 이벤트를 주기 위한 이벤트 핸들러 함수가 필요합니다.
id 와 password에 각각 이벤트 핸들러 함수를 만들어 주었습니다.
이벤트 핸들러 함수를 만들어 주었으니, 각 input에 onChange 이벤트도 등록해 줍니다.
이벤트를 등록했다면, 이제 변경된 값이 잘 들어오는지 확인하기 위해 이벤트 핸들러 함수에 코드를 추가해 보겠습니다.
💡
이벤트 확인해보기
console.log(event) : 해당 이벤트의 객체 구조를 확인할 수 있습니다.
console.log(event.target.value) : 이벤트가 발생한 요소에 입력된 값을 확인합니다.
그림 2-11
그림 2-12
이제 로그인 폼이 완성되었으니 코드를 실행시켜 보겠습니다.
id 와 password의 input에 값을 입력한 후에 Login 버튼을 클릭해보면 콘솔창에서는 어떤 일이 발생하게 될까요?
그림 2-13
그림 2-14
각 input에 값을 입력할 때마다 위와 같이 콘솔창에 메시지가 찍히는 것을 확인할 수 있습니다.
Login 버튼을 클릭하면 “로그인 버튼을 클릭했습니다.”라는 메시지와 함께 아래와 같은 alert 창이 뜨는 것을 확인할 수 있으며, alert 창에서는 각 input에 입력한 텍스트를 확인할 수 있습니다.
그림 2-15
이렇게 useState를 사용하여 state 상태를 관리하는 로그인 폼을 만들어 보았습니다.
useState를 사용하여 각 input의 값이 변경될 때마다 해당 컴포넌트를 자동으로 리렌더링 해주는 것을 확인할 수 있었습니다. 매번 모든 값이 변경될 때마다 컴포넌트를 업데이트 하면 많은 리소스가 낭비됩니다. 그렇기 때문에 React Hook에서 제공하는 useState를 통해 특정 상태(state)가 변경될 때만 해당 컴포넌트를 화면에 다시 렌더링 해주는 것입니다.