JAVASCRIPT/리액트
리액트 커스텀 훅 사용 시 다중상태관리와 클로저의 존재 이유
진짠
2024. 6. 12. 09:43
728x90
리액트의 컴포넌트
- 리액트의 컴포넌트는 함수와 클래스로 나뉜다.
- 요새는 함수 컴포넌트를 사용하는 추세라고 한다. 왜냐하면 클래스 컴포넌트는 변경에 취약하여 인스턴스로 만든 속성에 접근이 가능한 반면 함수는 내부 상태를 클로저로 관리하기 때문에 외부에서 접근하지 못한다.
- 또한 클래스 컴포넌트는 리액트 컴포넌트를 반드시 상속 받아야하고 생성자 super 호출 및 render메소드를 반드시 구현해야함.
- 함수 컴포넌트는 리액트 엘리먼트를 반환해야함.
- 클래스 컴포넌트는 상태를 관리하기 위해 this 객체를 사용한다.
- 반면 함수 컴포넌트는 상태관리에 한계가 있기 때문에 등장한 것이 훅(hook)이다.
훅(hook)
- 리액트의 useState를 사용하여 함수 컴포넌트의 state값을 변경한다. (클래스의 인스턴스의 경우 this.state를 통해 내부의 값을 변경할 수 있지만 함수의 경우 한번 실행되고 종료되는 특성 상 훅을 사용해야 한다.) -> 리랜더링 하는 과정이라고도 할 수 있다.
다중 상태 관리와 커스텀 훅
- 그렇다면 여러개의 상태를 관리해야 할때는 어떻게 할까?
- 해당 값을 가리키는 포인터 역할이 필요하다.
- 예제 소스는 인프런 김정환 선생님 것을 참고하였다.
**App.jsx**
function NameField() {
// 최초 랜더링 시 firstName과 lastName을 생성할때 MyReact의 useState를 호출해서 값을 세팅해준다.
// 이때 값은 firstName = “사용자1”, lastName = “김” 이 될 것이다.
const [firstName, setFirstName] = MyReact.useState("사용자1");
const [lastName, setLastName] = MyReact.useState("김");
// input 값 변경 시 실행되는 함수다. MyReact를 통해 생성된 set함수를 사용한다.
const handleChangeFirstName = (e) => {
setFirstName(e.target.value);
}
// input 값 변경 시 실행되는 함수다. MyReact를 통해 생성된 set함수를 사용한다.
const handleChangeLasttName = (e) => {
setLastName(e.target.value);
}
return <>
<input value={firstName} onChange={handleChangeFirstName} />
<input value={lastName} onChange={handleChangeLasttName} />
</>
}
export default () => <NameField />;
**MyReact.jsx**
import React from "react";
const MyReact = (function MyReact() {
const memorizedStates = [];
const isInitialized = [];
// cursor를 유의하며 본다.
// 이것이 다중 상태에서 각각의 객체를 판단할 수 있는 포인터 역할을 한다.
let cursor = 0;
function useState(initialValue = "") {
const { forceUpdate } = useForceUpdate();
// 최초 실행 시 isInitialized 변수를 이용하여 값을 세팅해준다.
// 다중 상태이기 때문에 이 변수 또한 배열로 관리해준다.
if(!isInitialized[cursor]) {
memorizedStates[cursor] = initialValue;
isInitialized[cursor] = true;
}
// state 변수에 최초로 생성될 당시 세팅된 값을 확인하는 역할을 한다.
const state = memorizedStates[cursor]
// setState의 구현 부분이다. _cursor를 유의하며 봤다.
// 최초 세팅 시 생성한 cursor값은 클로저로 세팅하여 setState안에 저장할 것이다.
// 그래야지 해당 setState를 호출했을 때 저장했던 cursor값을 확인하여 해당 값을 바꿀 수 있기 때문이다.
// 미리 저장한 cursor값으로 배열을 확인해서 변경된 값(nextState)으로 수정해준다.
// 그리고 재랜더링 단계를 통해 값을 그려준다.
const setStateAt = (_cursor) => (nextState) => {
if(state === nextState) return;
memorizedStates[_cursor] = nextState;
forceUpdate(); // 재렌더링
};
// setState를 사용시 setStateAt함수를 사용하며 이때의 cursor값을 가져가기 위해 클로저로 적용한다.
const setState = setStateAt(cursor);
// 차례대로 cursor값을 저정하기 위해 더해준다.
cursor = cursor + 1;
return [state, setState];
}
function useForceUpdate() {
const [value, setValue] = React.useState(1);
// 리액트의 상태가 바뀌면 강제로 재렌더링 된다는 것을 이용하여 React.useState로 값을 설정하고
// 그 값을 바꿔줌으로써 재렌더링한다.
// cursor는 0으로 초기화해야 재렌더링 시 차례대로 커서가 증가하며 저장했던 memorizedState에서
// 값을 꺼낼 수 있다.
const forceUpdate = () => {
setValue(value + 1);
cursor = 0;
}
return { forceUpdate };
}
return { useState };
})();
export default MyReact;
- 소스의 순서를 살펴본다.
-> App.jsx의 NameField를 최초 렌더링한다.
-> function NameField의 firstName, lastName을 MyReact.useState를 이용해 세팅한다.
-> MyReact의 function useState로 이동한다.
-> memorizedStates[cursor]에 초기값을 세팅한다.
-> const setState = setStateAt(cursor); 를 통해 cursor값을 클로저로 저장한다.
-> if(state === nextState) return; 초기값이므로 별다른 재렌더링 로직을 타지않고 바로 빠져나온다.
-> cursor = cursor + 1; 커서값을 증가시켜 배열에 차례대로 값을 넣어준다.
-> 다시 App.jsx로 돌아온다. 여기까지가 최초 페이지 로딩 시 과정이다.
-> 다음과 같이 value값이 들어가 있는 것을 알 수 있다.
-> 이제 onchange를 테스트하기 위해 텍스트값을 변경한다.
-> 두번째 텍스트를 변경했다고 가정하자.
-> handleChangeLasttName 가 호출되고 이는 setLastName을 호출한다.
-> e.target.value는 내가 현재 입력한 텍스트 값을 가져온다.
-> setLastName은 최초 렌더링 시 클로저인 cursor의 1에 저장되어 있다는 것을 기억하고 있다.
내가 이해를 못한 부분
- cursor는 MyReact.jsx에 선언되어있는 전역변수인데 어떻게 setState호출 시 기억한 값을 가져오는걸까? 클로저는 알겠는데 어떻게 적용이 되는거지?
- 혼자 이해의 물꼬를 튼 부분은 함수마다 변수를 저장하는 스코프가 다르다는 부분이었다.
- 최초 렌더링 시에는 cursor의 전역변수를 사용할 것이다. 그리고 1씩 증가하는 로직을 통해 순서대로 배열의 값에 저장될 것이다.
- 주의해서 봐야할 부분은 'const setState = setStateAt(cursor);' 이다.
- 이를 통해 최초 렌더링 시 cursor값은 '캡쳐' 된다. setState함수 자체에 cursor값이 저장되는 것으로 이해했다. 왜냐하면 setState호출은 곧 setStateAt에 저장된 cursor까지 같이 불러오니까.
- 이것은 setStateAt에서 _cursor 인수를 통해 로직으로 처리되고 재호출 시에도 마찬가지일 것이다.
- 클로저를 사용안하면 다음과 같은 코드가 된다.
const setState = (nextState) => {
if(state === nextState) return;
memorizedStates[cursor] = nextState;
forceUpdate(); // 재렌더링
}; - 해당 cursor변수를 선언하고 초기화하는 어떤 과정도 없기 때문에 전역변수인 cursor를 쓸 것이고 당연히 얘는 별도로 관리되지 않기 때문에 포인터의 역할을 할 수 없다.
- 이해를 마치고 다시 과정으로 넘어갔다.
-> cursor값을 참조하여 해당하는 값에 접근한 다음 변경한다.
-> 재랜더링한다.(forceUpdate();)
-> const [value, setValue] = React.useState(1); 이부분을 참고한다. 리액트는 상태가 바뀌면 강제적으로 재랜더링 되는 규칙을 이용한다. 이를 통해 값을 바꾸고 재랜더링 시킨다.
-> cursor값을 0으로 초기화한다. 그래야 재랜더링 시 저장해놓은 값(memorizedStates[cursor]) 에 순서대로 접근하여 가져올 수 있기 때문이다.
-> 재랜더링을 통해 변경된 값이 const state = memorizedStates[cursor] 여기에 차례대로 담긴다.
-> 반환된 state는 {lastName} 에 반영된다.
- 다음과 같은 과정을 통해 값을 바꾸면 value의 값이 바뀌는 것을 알 수 있었다.
(선생님의 훌륭한 강의를 들으며 어려운 리액트를 쉽게 접근하고 배울 수 있어 감사드린다.)
728x90