목록으로
UI 프로그래밍에서 상태 관리란?
UI 요소는 다양합니다. 버튼, 드랍다운 메뉴, 체크 상자, 텍스트 출력 요소 등. 이러한 UI 요소는 다양한 상태(State)일 수 있습니다. 예를 들어, 버튼을 클릭한다고 했을 때, 버튼은 다음 상태 변화 과정을 거칩니다.
-
버튼이 눌려진 상태
-
버튼이 눌려진 상태에서 해제되는 상태
-
원래 상태.
UI 요소의 상태는 UI 요소의 역할, 즉 기능에 따라 다양한 상태를 갖습니다. UI 요소를 객체(Object)라고 비유하면, 상태는 객체의 멤버 변수라고 할 수 있지요. UI 요소는 대개 복잡한 상태를 갖습니다. 회원가입 양식(form)을 예로 들게요. 이 양식도 UI 요소입니다. Form이라고 하겠습니다. Form엔 사용자ID, E-mail 주소, 비밀번호, 그리고 비밀번호를 확인하는 값을 입력 받으며, 회원가입 버튼이 UI 요소로 포함되어 있습니다.
-
네 개 입력 UI 요소 중 어느 하나라도 입력을 하지 않으면 가입 버튼은 활성화되지 않습니다.
-
모두 입력했더라도 비밀번호와 비밀번호 확인 값이 일치하지 않으면 가입 버튼은 활성화되지 않습니다.
-
사용자ID는 네 글자 이상 입력해야 입력한 것으로 간주합니다.
-
E-mail 주소는 유효한 E-mail 형식이어야 입력한 것으로 간주합니다.
-
비밀번호는 여덟 글자 이상 입력해야 입력한 것으로 간주합니다.
-
비밀번호는 사용자ID와 달라야 입력한 것으로 간주합니다.
-
가입 버튼을 누르면 모든 입력 UI 요소가 비활성화 되어 수정할 수 없습니다.
-
서버로부터 가입 요청에 대해 유효하지 않은 값이라는 응답을 받으면 모든 입력 UI 요소는 활성화 상태가 됩니다.
사용자 요구사항에 따라 얼마든지 더 복잡해집니다. 의심이 많은 분이라면 위에 나열된 내용엔 UI 상태(State)와 조건(Condition)이 섞여있다는 걸 눈치채실지도 모르겠군요. 각 UI 요소는 객체로써 다른 객체와 협력하고, 협력에 따라 상태가 달라진다는 걸 설명하려는 의도였습니다. 🙂
회원가입 버튼을 상태 변화하는 과정에 놓고 보죠. 우선 비활성화 상태일 때는 어떤 모양일까요? 대개는 회색 바탕이고 마우스 커서를 올려도 바탕색 변화가 없어서 마치 벽돌처럼 굳은 것처럼 표현하지요. 이 상태에선 버튼을 눌러도 아무 반응이 없고요. 활성화 상태일 땐, 창량한 푸딩캠프의 상징인 파란색이 배경에 깔리고, 마우스 컬러를 올리면 좀 더 연한 파란색으로 바탕이 바뀝니다. 클릭하면 이 버튼이 클릭될 때 수행할 동작이 호출되고요.
이 화면을 매우 낮은 수준으로 내려가 상상해보세요. 운영체제의 제어에 따라 모니터에 버튼을 그립니다. 처음엔 비활성 상태이니 회색 배경으로 회원가입 버튼이 그려집니다. 이제 회원가입 버튼이 활성화되는 조건을 만족해서 활성 상태로 그립니다. 파란 배경이 되지요. 우리 눈에는 버튼의 색상이 교체되는 것으로 보이지만, 실제로는 그 위치에 파란색 버튼으로 새로 그립니다. 그래픽 카드라는 하드웨어 입장에선 버튼이라는 객체는 존재하지 않습니다. 요청받은 좌표에 픽셀 하나 하나 빛을 발광하도록 만들어 그리는 것 뿐입니다. 이 과정이 상상 속에서 그려진다면 이번 편에서 다룰 내용의 핵심을 이해하신 겁니다!
리액트에서 상태 관리
상태 변화에 따른 UI 컴포넌트 처리하는 과정
리액트가 UI 요소인 컴포넌트의 상태를 다루는 방식도 앞선 설명과 크게 다르지 않습니다. 왜냐하면 앞선 설명은 실은 리액트의 상태를 설명하려고 밑그림처럼 설명을 쓴 것이거든요. 😅 용어부터 살펴볼까요? 리액트는 컴포넌트의 상태가 변하면 렌더링(rendering)합니다. 하지만 아직 화면에 그리진 않습니다. 회원가입 버튼이 활성 상태가 되어서 렌더링은 하지만, 하지만 파란 배경으로 그린 건 아닙니다. 그리고 잠시 후, 잠시라고 하기엔 우리가 거의 인지하지 못할 정도로 짧은 시간이 지난 후에 비로소 바뀐 상태에 맞춰 그리는데, 이를 페인팅(painting)이라 합니다.
페인팅은 리액트가 웹 브라우저에게 요청해서 이뤄지며, 꽤 느렸습니다. 느렸다고 표현하는 이유는, 최신 브라우저는 최적화가 잘 되어 있어서 과거보다 더 빠르게 페인팅, 브라우저 입장(?)에선 DOM 처리를 하기 때문입니다. 어쨌든 리액트가 개발되고 한참 동안은 DOM을 변형하는 건 꽤 느린 작업이었습니다. 그래서 리액트는 렌더링이나 리-렌더링(re-rendering) 처리를 할 때마다 페인팅을 하진 않고, 렌더링 정보를 모았다가 일괄 처리(batch)합니다. 이 과정은 비동기(Asynchronous)로 처리되지요. 이에 대해서는 아래에서 따로 살펴 보겠습니다.
그럼 리액트는 언제 어떻게 렌더링을 할지 판단하는 걸까요? 리액트는 컴포넌트를 렌더링하면(렌더링이 화면에 그리는 동작이 아니라는 걸 유의하세요!), 렌더링된 결과물을 사진 찍듯이 보관하는데, 이 단위를 스냅샷(Snapshot)이라고 합니다. JavaScript에서 동작하니 JavaScript 객체입니다. 자, 이제 회원가입 버튼이 활성 상태로 변경되었습니다. 이 변경을 재조정(reconciliation)이라고 합니다. 재조정이 일어나면 비동기로 변경된 정보가 리액트의 대기열(queue)에 쌓여 들어가고, 일괄 처리(batch) 때가 되면 리액트는 스냅샷 찍어 놓은 컴포넌트 객체(React-dom)와 상태가 조정된 객체를 비교합니다. 만약 상태가 3개가 바뀌었으면 이들을 한꺼번에 반영하며, 최종적으로 DOM 을 조작하여 웹 브라우저가 해당 UI 요소(회원가입 버튼)를 새로 그립니다(페인팅).
어려우신가요? 거의 다 왔어요. 힘냅시다!
useState() 함수로 상태 관리하기
리액트 Hooks이란? 편에서 리액트 훅으로 리액트의 컴포넌트의 상태를 관리한다고 설명하였습니다. 잠시 우리가 작성한 코드를 꺼내보겠습니다. 오랜만이군요!
코드 중에서 useState() 함수가 상태 값을 다루는 리액트 훅입니다. 이 함수를 실행하면 두 개 객체를 배열에 담아 반환하는데, 첫 번째 객체는 상태의 값을 담고 있습니다. 두 번째는 상태의 값을 변경하는 함수입니다. setEmail() 함수로 상태의 값을 변경하면, email의 값이 바뀌지요. 리액트는 setEmail()가 호출되는 것으로 상태 변화를 감지합니다. email의 값을 바꾸는 것으로는 반응하지 않지요.
그런데 신기하다고 느끼지 않으시나요? Subscription 컴포넌트는 리액트 입장에서나 컴포넌트이지, 이 코드가 구동되는 환경은 JavaScript라는 언어이고, JavaScript 입장에서 Subscription은 함수일 뿐입니다. 함수는 실행하면 인자를 전달받으면서 시작되고, return문을 만나 값을 함수 호출자에게 반환하면서 실행이 끝납니다. 매번 이렇게 동작합니다. 그런데 useState() 함수를 쓰면 마치 함수의 특정 위치에서 시작하는 것처럼, 좀 더 정확하게는 함수 내부의 값이 보존되는 것처럼 동작하는 것이거든요. 아까 표현한 것처럼 스냅샷을 뜬 것처럼 동작하는 것인데, 어떻게 함수가 이렇게 동작하는 걸까요?
바로 이 점이 useState() 훅, 더 넓게는 리액트 훅의 역할이자 의의입니다. 바로 선언형 프로그래밍(declarative programming)이지요. 어떤 마법을 부리는지는 사용자가 알 필요 없고, 우리가 필요한 위치에 useState()로 상태를 선언해놓으면 그만이죠. 이에 대해서는 Props로 자식 Element 전달하기 편에서도 설명한 바 있습니다.
비동기로(Asynchronous) 처리되는 상태 변화
앞서 제가 리액트는 상태 처리를 비동기로 처리한다고 말씀드렸는데요. 실습으로 확인해보겠습니다. Subscription 컴포넌트를 조금 고치겠습니다.
어디가 바뀌었냐면, onChange 쪽입니다. 자, 우선 hannal@puddingcamp.com을 입력해보세요. 웹 브라우저에서 개발자 도구는 일단 무시하시고요. 이제 콘솔을 청소(clear)하시고요. 백 스페이스를 한 번 누르세요. 그럼 hannal@puddingcamp.com이 아니라 hannal@puddingcamp.co이 됩니다. 그럼 email에도 이 내용이 반영됐을테니 console.log(email)코드로 인해 웹 브라우저 콘솔에는 hannal@puddingcamp.co가 찍힐 겁니다. 해보시면 아시겠지만, 실제로는 hannal@puddingcamp.com이라는 변경 전 값이 찍힙니다. 리액트가 상태 변화 처리를 비동기로 하는 사이에 console.log() 코드가 실행되어서 그렇습니다.
이 동작은 여러분이 숙지해야 합니다. 대부분 사람에겐 비동기보다 동기식 동작을 머릿 속에 그리는 게 쉽고 익숙하고 친숙합니다. 그래서 리액트가 상태 처리를 동기식으로 한다고 머리에 심성 모형이 그려져 있다면 버그를 만들 가능성이 커집니다. 간단히 말해 실수하기 일쑤입니다. 특히 상태가 한 컴포넌트 안에 복잡하게 서로 얽히면 더욱 그럴 실수할 가능성이 크지요.
마치기 전에, 위 코드에서 어떻게 입력하는대로 콘솔을 찍을 수 있는지 알려 드릴까요? 아주 간단합니다.
허무할 정도로 시시하죠? 😅 newValue과 email은 엄밀히 말해 좀 다른, 뭐랄까, 거주지가 다르달까요. email은 리액트의 상태 관리 체계에 있는 객체이고, newValue는 내용물(값)이 같다는 걸 이용해서 출력에 활용한 것뿐이지요.
푸딩캠프 뉴스레터를 구독하면 학습과 성장, 기술에 관해 요약된 컨텐츠를 매주 편하게 받아보실 수 있습니다.
목차