오늘은 챌린지반 수업을 듣고 관련 과제를 하고, 다른 사람의 라이브코딩을 보면서 시간을 보냈다. 과제제출까지 일주일이 남았는데 60%를 끝냈기 때문에 보일 수 있는 여유였다.
특히 다른 사람의 코드를 보는 것이 재미있었는데, map을 돌릴 때 list에서 특정 값만 가지고 와서 물려주지 않고도 list 자체만으로도 컴포넌트를 구성하는 것이 인상적이었다.
또 card에서 추가/삭제 버튼을 구성하는 방법 또한 흥미로웠는데, 상위 컴포넌트에서 해당 card 컴포넌트를 불러올 때 물려주는 값에 따라서(그 분은 true false로 props를 다르게 했다) 다른 버튼이 반환되도록 코드를 짠 것이다. 여태까지는 props를 물려줄 때 키와 값을 똑같이 하는 것만 해왔기 때문에 이렇게 키는 똑같이 하되 호출 지점에 따라 다른 값을 가지도록 하는 것은 신선한 아이디어라고 느꼈다.
그리고 오늘의 하이라이트로 넘어가자.
오늘의 과제는 리액트 hook 만들기였다. 여태껏 hook을 쓰면서도 이게 어떻게 작동하고 있는지 잘 모르겠어서 항상 불안하게 생각했었는데, 이번 기회에 hook을 직접 만들어보면서 hook의 구조에 대해서 확실하게 이해하게 된 것 같다.
핵심은 여태껏 배운대로 'React Hook은 배열로 관리되고 있다'는 것이다.
해당 사실을 쉽게 알기 위한 코드 표기를 잠깐 보겠다.
hook의 상태값이 배열로 저장되는 모습
const [count, setCount] = useState(0); // hooks[0]에 count값 저장
const [text, setText] = useState("foo"); // hooks[1]에 text값 저장
useEffect(() => {
console.log("effect", count, text);
return () => {
console.log("cleanup", count, text);
};
}, [count, text]); //hooks[2]에 객체 배열 형태로 값 저장
위 코드를 보면 훅이 선언되는 순서에 따라 hooks라는 배열에 값이 저장되고 있다고 주석을 달았다.
이게 가능하기 위해서는 몇 가지 과정이 필요한데, 아래가 그것이다.
인덱스++
// 상태값을 배열로 선언
let hooks = [];
// 상태값 배열의 '인덱스 선언'
let currentHook = 0;
// useState의 반환값
return [hooks[currentHook++], setState];
// useEffect의 마지막 코드
currentHook++;
hook이 호출될 때마다 currentHook을 1씩 증가시켜서 각 hook의 값을 같은 배열에서 순서대로 저장되도록 하는 것이다. 다만 이렇게 hook을 배열로 관리하면 호출순서가 굉장히 중요하기 때문에 hook을 불렀다 안 불렀다 하며 호출순서를 꼬이게 할 수 있는 'if문'이나 의도치않은 리렌더링으로 인해 호출순서를 꼬이게 할 수 있는 '하위컴포넌트에서 hook 사용'이 지양되어야 한다.
다만 이렇게 사용하는 것에도 또 문제가 있다. 리액트의 hook과 리렌더링은 이전값을 기억하기 때문에 이런식으로 currentHook++만을 사용해서 값을 저장할 경우 잘못된 위치에 값을 저장할 수 있다.
useState로 예를 들어보자.
첫번째 렌더링 시에 초기값으로 one을 state에 저장했다고 하자. 그러면 hooks[0]에 값이 저장된다.
hooks= ['one']
state = 'one'
이 상태에서 위처럼 ++로 인덱스를 올리면서 setState('two')를 사용하여 리렌더링을 하면 hooks[1]에 값이 저장된다.
hooks= ['one','two']
이렇게 값이 저장이 되기는 했지만 useState가 가리키는 것은 hooks[0]이기 때문에 state는 계속해서 'one'을 가리키게 된다.
state = 'one'
상태값이 업데이트되지 않는 것이다.
때문에 리렌더링하여 hook을 다시 불러오기 전에 인덱스를 리셋해줄 필요가 있다. 아래가 그 부분이다.
인덱스 초기화
const MyReact = {
render(Component) {
const instance = Component();
instance.render();
//이 부분
currentHook = 0;
//이 부분
return instance;
},
};
이 코드로 인해 리렌더링 시마다 hook들의 인덱스가 초기화되면서 hooks[0]부터 차근차근 값을 저장할 수 있다.
이제 업데이트가 가능하다.
hooks= ['one']
state = 'one'
setState('two')
hooks= [' two ']
state = ' two '
여기까지 hook이 배열로 관리되고 있는 모습을 살펴보았다. 여태껏 이유 모르고 알고 있던 개념인 'hook은 배열로 관리되고 있고, 그렇기 때문에 상위컴포넌트에서 사용해야한다'를 이번에 확실하게 이해하게 되었다.
첨언하자면, 이 모든 과정이 가능한 것은 또 하나, 클로저 (Closure)라는 개념 덕분이다. useState 등으로 불러오는 함수는 해당 함수가 내부에 가지고 있는 지역 변수뿐만 아니라 전역변수 또한 기억하고 있다. 때문에 전역변수인 hooks와 currentHook을 활용하여 값을 저장할 수 있는 것이다. 마지막으로 내가 오늘 완성한 hook 코드들을 명시하며 클로저에 대한 이해를 돕고 마치도록 하겠다.
// 상태값을 배열로 선언
let hooks = [];
// 상태값 배열의 '인덱스 선언'
let currentHook = 0;
// 호출순서
const useState = (initialValue) => {
//초기값 설정
hooks[currentHook] = hooks[currentHook] || initialValue;
const hookIndex = currentHook;
//setState 함수 선언
const setState = (newState) => {
if (typeof newState === "function") {
hooks[hookIndex] = newState(hooks[hookIndex]);
} else {
hooks[hookIndex] = newState;
}
};
//useState 기능을 사용할 수 있는 값과 함수를 배열로 반환하여 순서를 맞추면 이름이 바뀌어도 같은 기능을 하도록 함
return [hooks[currentHook++], setState];
};
다른 파일에서 useState를 실행했지만 이전파일 전역변수인 hooks가 활용되고 있는 모습
const [count, setCount] = useState(0); // hooks[0]에 count값 저장
const [text, setText] = useState("foo"); // hooks[1]에 text값 저장
'til' 카테고리의 다른 글
리액트 숙련 5일 차(完) @개인 과제 200% redux-toolkit, memoization, toaster (0) | 2024.08.23 |
---|---|
리액트 숙련 4일 차 @개인과제 100% (0) | 2024.08.22 |
cra, vite 없이 리액트 프로젝트 생성하기 (0) | 2024.08.20 |
리액트 숙련 3일 차 @개인과제 60% (0) | 2024.08.20 |
리액트 숙련 2일 차 @개인과제 10% && react-router-dom (0) | 2024.08.19 |