React 훅
React는 훅(Hooks)이라는 기능 집합을 제공한다. 아들을 사용하면 컴포넌트의 상태를 관리하거나 클릭에 대한 동적인 반응, 데이터 업데이트와 같은 작업이 가능해진다. 즉 동적인 웹 구성요소로 거듭나게 하는 매우 중요한 기능인것이다.
I. useState
useState 훅은 상태 변수와 변수의 setter 함수 및 초기화를 한줄로 빠르게 구성하는 기능이다.
이 훅을 사용하면 isLoggedIn 값의 변화에 따라 다른 텍스트를 렌더링하는 등의 동적인 작업을 간편하게 구현할 수 있다.
이 부분을 유심히 볼 필요가 있는데, 상태 변수인 isLoggedIn에 타입을 전혀 지정하지 않고 있다. 이것은 타입스트립트 규칙에 위배되는것처럼 보이지만, 오류는 없다. 왜냐하면 useState()안에 할당한 false 값을 보고 자동으로 isLoggedIn 이 boolean 타입이라고 추론하기 때문이다.
그래서 타입 규칙은 정확히 작동한다. setIsLoggedIn() 호출시 문자열이나 숫자를 넣으려고 하면 오류가 발생한다.
타입 명시
이런 상황을 생각해보자. 로그인 버튼을 누르면 이름, 이메일에 입력한 정보가 채워지지만 그 전에는 값 자체가 없어야한다. 또한 로그아웃을 누르면 값이 모두 사라져야 한다. 즉 null이 필요한 경우이다.
useState()로 이 경우를 관리한다면 타입 추론에 의존해서는 안된다. 초기화 시 당연히 useState(null)일텐데, 이러면 상태 변수는 null 타입으로 추론해버린다.
그래서 user가 "AuthUser일 수도, null 일 수도 있음"을 컴파일러에 전달하기 위해 꺽쇄 기호 <>를 사용하여 타입을 명시한다.
타입 주장
위와 같은 상황에서, 초기값을 항상 null로 주게되면 해당 상태 변수를 사용할땐 이게 null일수도 있음을 염두에 두어야 하고, null일 경우에 대비한 코드도 짜야 한다. 하지만 프로그램을 만들다 보면 구조적으로 null일수가 없는 상황이 있다. 로그인의 경우 ID나 비번 입력이 없으면 벼튼 자체를 비활성화 할수도 있다. 이처럼 상태 전환이 일어났을 때 null이 아닐 것임이 기약된 상황에서, 타입 주장을 통해 상태 변수가 null타입일 가능성을 없애버릴 수 있다. 즉 타입스크립트에게 "이 상태 변수는 null인적이 없어. 처음부터 AuthUser 타입이야!" 라고 거짓말을 하는 것이다.
II. useReducer
useState 훅은 간단한 상태 값을 다루기에 좋다. 하지만 다음 상태가 이전 상태에 따라 달라지는 복잡한 로직의 경우 useReducer를 사용하는것이 좋다.
수치가 누적되는 상태 변수가 필요하다면, setter 함수는 단순히 상태변수에 새 값을 쓰는게 아니라 이전 상태변수를 가져와 연산을 하는것이 좋을것이다. 따라서 다음과 같이
useReducer 훅으로 setter 함수가 호출할 다른 함수를 새로 정의하고 원하는 로직을 짤 수 있다. 이 때 새로운 함수는 첫번째 인자로 이전의 상태 변수를 받는다.
매개변수가 조금 복잡하게 보일 수 있어 그림으로 표현하자면 다음과 같다.
flowchart useReducer -->|state| dispatch onClick -->|action| dispatch dispatch -->|state, action| reducer
그리고 최종 호출자인 reducer에서는 action 객체의 type을 확인하고 payload만큼 뺴거나 더하는 로직이 이루어진다.
유니온 타입
이제 상태를 자유자재로 변화시킬 수 있는 useReducer훅이라는 유용한 기능을 알게 되었다. 근데 이 상태변수가 어떤 상황에는 초기값(0)으로 초기화될 필요가 있다. 그러한 action도 reducer에 전달해보고자 한다.
근데, 이 경우 action에는 payload라는 속성이 전혀 필요하지 않다. 그래서 dispatch를 호출할 땐 payload에 무의미한 값을 전달하거나 CounterAction 타입의 payload 속성을 ?로 선언하여 필수가 아님을 타입스크립트에 알려야한다.
근데 이 방식이 최선일까? 임시 방편으로 돌려막기하는 그런 코드는 의도가 명확하지 않으며 가독성을 떨어뜨리고 유지보수를 어렵게 한다.
따라서, reducer 함수가 받을 CounterAction 타입은 payload가 포함된 증감 action이거나, payload가 없는 초기화 action일수도 있다고 타입스크립트에게 알려야 한다. 즉 서로 다른 두 타입을 합쳐 "이거일수도 있고 저거일수도 있음"을 명시하는 것인데 이걸 유니온 타입이라고 한다.
유니온 타입이 된 CounterAction은 dispatch를 호출하는 시점에서, 인수에 payload 속성이 있어도 되고 없어도 된다. 있으면 UpdateAction일거라 추론하고, 없으면 ResetAction일거라 추론한다.
III. useContext
Context는 전역적인 데이터를 관리하기 위한 객체이다.
리액트는 여러 컴포넌트들이 쌓이고 쌓여 웹페이지를 완성하는 식으로 작동하는다. 그리고 props를 주고 받고 함으로써 동적으로 테마를 변경하거나 사용자의 정보를 관리한다. 그런데 props를 전달할 땐 한 단계를 넘어 전달할수는 없다. 그래서 여러 단계를 거쳐 매우 안쪽에 있는 자식까지 props를 전달하려면 중간에 껴있는 컴포넌트들이 받아서 또 전달하고를 반복해야 한다. 이것을 "Props Drilling" 이라고 한다. 컨텍스트는 이 Props Drilling을 해결하기 위해 사용한다.
컨텍스트 만들기
테마 정보를 공급하는 컨텍스트를 만들어 볼것이다. 웹페이지에 있는 다크모드 온/오프 기능처럼, 여러 컴포넌트에 한번에 적용되어야 하는 테마 스타일은 흔한 컨텍스트 활용 예시이다.
먼저 테마 정보를 담는 오브젝트를 간단하게 만든다. 컨텍스트가 제대로 작동한다면, 웹 페이지 사용자는 테마를 primary나 secondary로 자유롭게 바꿀 수 있을 것이다.
그리고 createContext를 불러와 컨텍스트를 만듦과 동시에 Provider도 정의한다. Provider 역시 하나의 리액트 컴포넌트지만 실제로 렌더링되는 요소는 아니다.
props의 타입들 중에는 React.ReactNode가 꼭 있어야 한다. 컨텍스트를 사용할 컴포넌트를 받아야 하기 때문.
그리고 JSX 구문에서 반환하는 ThemeContext.Provider의 value prop이 바로 우리가 정의한 테마를 전달하는 인자이다.
컨텍스트 사용하기
컨텍스트를 사용하는 컴포넌트인 Box에서는 useContext와 우리가 만든 ThemeContext를 불러와 theme 값을 전달받아 스타일에 사용하게 된다.
컨텍스트로 전달받은 value 객체는 기본적으로 읽기 전용(Read-Only) 객체이다. 따라서 값을 직접 변경하는것은 불가능하다. 대신 setter 함수를 만들어 함께 컨텍스트로 전달할 수 있다. 이 내용은 뒤에서 자세히 다루어본다.
컴포넌트 배치하기
컨텍스트를 사용할 컴포넌트들을 배치할 땐 아래와 같이 ContextProvider 컴포넌트의 자식으로 오게 하면 된다.
컨텍스트 업데이트
테마같은 경우 웹페이지 제작자가 미리 정해놓고 고르게 하는 것이므로 업데이트가 필요하지 않다. 근데 로그인 정보를 생각해보자. 사용자가 로그인함으로서 업데이트되는 이름과 이메일 주소같은 정보는 테마처럼 미리 정해놓을 수 없다. 이처럼 어떤 데이터는 사용자의 입력등에 의해 업데이트가 필요하다. 컨텍스트로 이 업데이트를 수행하려면 컨텍스트에 setter 함수를 포함시켜 전달해야 한다.
그래서 이전처럼 createContext를 그냥 쓰면 안되고 컨텍스트 타입을 새롭게 정의해야 한다.
그래서 총 세 가지의 타입 정의가 필요하다.
| 구분 | 용도 |
|---|---|
| 데이터 타입 | 컨텍스트로 전달할 데이터의 타입 |
| 컨텍스트 타입 | 컨텍스트 자체의 타입(데이터+setter) |
| Props 타입 | Provider가 받을 인자(자식 컴포넌트) 타입(ReactNode) |
이제 해당 컨텍스트를 전달받은 컴포넌트에선 다음과 같이, setter로 함께 전달 setUser를 호출해 컨텍스트의 데이터를 업데이트 할 수 있게된다.