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를 호출해 컨텍스트의 데이터를 업데이트 할 수 있게된다.
IV. useRef
리액트에서 Ref는 어떤 값을 담거나 DOM 요소를 참조할 수 있는 객체이다.
HTML 요소 제어
Ref를 활용하는 대표적인 방법 중 하나는 HTML 요소에 대한 직접적인 제어 로직이다.
아래 컴포넌트는 Ref를 사용해 input 요소를 참조하여 저장한다. 해당 컴포넌트가 렌더링 된 이후 useEffect가 실행되면서, 해당 HTML 요소에 focus를 맞추게 된다.
useEffect는 컴포넌트가 렌더링 된 이후의 보조적인 동작을 실행시키는 훅이다. JSX 이전의 코드는 렌더링되기 전이므로 DOM 요소에 접근할 수 없지만, useEffect 내에선 렌더링 이후의 실행이 보장되므로 DOM 요소에 접근해 보조적인 작업을 처리할 수 있다.
또 useEffect 함수의 반환으로 함수 객체를 정의하여, 페이지를 떠날 때의 작업을 구현할수도 있다. 값을 초기화하거나 타이머처럼 지속적으로 수행되는 동작을 중지하는 등의 작업이다. '클린 업' 로직이라고 한다.
자유도 높은 전용 저장소
두번째 활용 방법으로는 컴포넌트에서 지속적으로 사용할 어떤 데이터를 저장하고 관리하는 로직이다.
위 예제에서 Ref는 타이머 유틸리티 객체를 생성하면서 받은 타이머 ID를 저장하는데 쓰인다. Ref는 State와 달리 값의 변경이 일어나도 다시 렌더링되지 않는다. 리액트의 규칙으로부터 자유로운 것인데, 따라서 객체 ID같은 UI와 무관한 어떤 값을 저장하고 관리하는데 사용함으로서 불필요한 리렌더링을 줄인다.
State는 리액트의 규칙을 따르는 객체로서, setter에 의해 값의 변경이 일어나고 값이 변경되면 컴포넌트 전체가 다시 렌더링된다. 따라서 State는 UI와 직접적인 연관이 있는 데이터를 저장하는데 쓰인다.
Ref는 리액트 규칙으로부터 자유로워 렌더링을 발생시키지 않아 조용하고 효율적인 데이터 처리가 가능하다.
또 Ref는 메모리 주소의 불변성을 가진다. 힙 영역에 새로운 객체를 하나 만들고 current 속성으로 참조하는 원리이다. 따라서 일반 지역변수처럼 컴포넌트 호출시마다 초기화되지 않고 정적으로 데이터를 관리할 수 있다.