오늘은 내가 공부한 React Hooks들을 정리해보았다.
useState는 가장 기본적인 hook이며, 일반 기존 업데이트 방법과 함수형 업데이트 방식이 있다. 함수 컴포넌트에서 가변적인 상태를 가지게 해준다.
useState의 기본적인 형태
const [state, setState] = useState(initialState);
useState 라는 함수가 배열을 반환하고, 이것을 구조 분해 문법으로 꺼내놓은 모습.
state를 변수로 사용했고, setState를 이용해서 state 값을 수정할 수 있다.
그리고 만약 state가 원시 데이터타입이 아닌 객체 데이터 타입인 경우에는 불변성을 유지해줘야 한다.
setState를 사용하는 방식에는 일반적인 방식말고 또 다른 방식이 있다. 함수형 업데이트 방식이다.
// 기존에 우리가 사용하던 방식
setState(number + 1);
// 함수형 업데이트
setState(() => {});
setState의 ( ) 안에 수정할 값이 아니라, 함수를 넣을 수 있다.
그리고 그 함수의 인자에서는 현재의 state을 가져올 수 있고,
{ } 안에서는 이 값을 변경하는 코드를 작성할 수 있다.
// 현재 number의 값을 가져와서 그 값에 +1을 더하여 반환한 것 입니다.
setState((currentNumber)=>{ return currentNumber + 1 });
먼저 일반 업데이트 방식으로 onClick안에서 setNumber(number + 1) 를 3번 호출해면. number가 1씩 증가한다.
// src/App.js
import { useState } from "react";
const App = () => {
const [number, setNumber] = useState(0);
return (
<div>
{/* 버튼을 누르면 1씩 플러스된다. */}
<div>{number}</div>
<button
onClick={() => {
setNumber(number + 1); // 첫번째 줄
setNumber(number + 1); // 두번쨰 줄
setNumber(number + 1); // 세번째 줄
}}
>
버튼
</button>
</div>
);
}
export default App;
이번에는 함수형 업데이트 방식으로 동일하게 작동시켜보자. number가 3씩 증가한다.
// src/App.js
import { useState } from "react";
const App = () => {
const [number, setNumber] = useState(0);
return (
<div>
{/* 버튼을 누르면 3씩 플러스 된다. */}
<div>{number}</div>
<button
onClick={() => {
setNumber((previousState) => previousState + 1);
setNumber((previousState) => previousState + 1);
setNumber((previousState) => previousState + 1);
}}
>
버튼
</button>
</div>
);
}
export default App;
왜 다르게 동작할까?
일반 업데이트 방식은 버튼을 클릭했을 때 첫번째 줄 ~ 세번째 줄의 있는 setNumber가 각각 실행되는 것이 아니라, 배치(batch)로 처리한다. 즉 우리가 onClick을 했을 때 setNumber 라는 명령을 세번 내리지만, 리액트는 그 명령을 하나로 모아 최종적으로 한번만 실행시킨다. 그래서 setNumber을 3번 명령하던, 100번 명령하던 1번만 실행된다.
반면에 함수형 업데이트 방식은 3번을 동시에 명령을 내리면, 그 명령을 모아 순차적으로 각각 1번씩 실행시킨다. 현재 스테이트0에 1더하고, 그 다음 현재 스테이트 1에 1을 더하고, 현재 스테이트 2에 1을 더해서 3이라는 결과가 우리 눈에 보인다.
-리액트는 성능을 위해 setState()를 단일 업데이트(batch update)로 한꺼번에 처리할 수 있다.
공식문서의 설명처럼, 불필요한 리-렌더링을 방지(렌더링 최적화)하기 위해 즉, 리액트의 성능을 위해 한꺼번에 state를 업데이트 한다.
손님이 피자, 콜라, 피클을 주문한다고 하자. 웨이터는 손님이 피자! 콜라! 피클! 하나씩 말할 때 마다 주방으로 달려가진 않는다. 손님이 세 가지의 주문을 모두 완성했을 때, 그 때 한꺼번에 주방으로 간다. 그래야 동선이 가장 짧다.
-useState의 업데이트 방식은 2가지 방식이 있으며, 각각 다르게 동작한다.
-useState 로 원시데이터가 아닌 데이터를 변경할때는 불변성을 유지해야 한다.
useEffect는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook
쉽게 말해 어떤 컴포넌트가 화면에 보여졌을 때 내가 무언가를 실행하고 싶다면? 또는 어떤 컴포넌트가 화면에서 사라졌을 때 무언가를 실행하고 싶다면? useEffect를 사용한다.
useState와 마찬가지로 React에서 제공하는 훅 (기능) 이므로, import React, { useEffect } from "react"; 로 import 해서 사용한다.
브라우저에서 우리가 App 컴포넌트를 눈으로 보는 순간, 즉 App 컴포넌트가 화면에 렌더링될 때 useEffect 안에 있는 console.log가 실행된다. 컴포넌트가 렌더링 될 때 실행된다. 이게 바로 useEffect 핵심 기능
useEffect(fuction(){});
//useEffect(괄호안에는 매개변수로 콜백함수가 들어간다.)
// src/App.js
import React, { useEffect } from "react";
const App = () => {
useEffect(() => {
// 이 부분이 실행된다.
console.log("hello useEffect");
});
return <div>Home</div>;
}
export default App;
앞서 배웠다시피, useEffect는 useEffect가 속한 컴포넌트가 화면에 렌더링 될 때 실행된다. 이런 useEffect의 특징에 의해 우리가 의도치않은 동작을 경험할수도 있다.
다음 코드를 보면, input이 있고 value 라는 state를 생성하여 input과 연결시켰다. 이렇게 구현하고 브라우저에 input에 어떤 값을 입력하면 useEffect가 계속 실행되는 것을 볼 수 있다.
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
useEffect(() => {
console.log("hello useEffect");
});
return (
<div>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</div>
);
}
export default App;
전체 흐름은 아래와 같다.
1.input에 값을 입력
2.value, 즉 state가 변경
3.state가 바뀌었기 떄문에 ->App 컴포넌트가 리렌더링
4.리렌더링 -> useEffect()
5.1~4번 계속 반복
6.Dependency Array가 필요. (처음 렌더링 될 때 한 번 만 적용 위해)
useEffect에는 의존성 배열이라는 것이 있다. 쉽게 풀어 얘기하면. 이 배열에 값을 넣으면 그 값이 바뀔 때만 useEffect를 실행한다 는 말이다.
// useEffect의 두번째 인자가 의존성 배열이 들어가는 곳 입니다.
useEffect(()=>{
// 실행하고 싶은 함수
}, [의존성배열])
우리가 위에 보았던 코드와 동일한 코드이다. 다만 useEffect에 의존성 배열만 추가했다. 이것을 추가함으로 어떻게 될까?
일단 의존성 배열 안에는 어떠한 값도 넣지 않았다. 의존성 배열이 “이 배열에 값을 넣으면 그 값이 바뀔 때만 useEffect를 실행할게” 라는 의미를 가진다고 했고 내가 아무것도 넣지 않았으니 useEffect는 처음에 딱 한번만 실행되고 그 이후로는 어떤 일이 일어나도 실행이 되서는 안된다.
// src/App.js
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
useEffect(() => {
console.log("hello useEffect");
}, []); // 비어있는 의존성 배열
return (
<div>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</div>
);
}
export default App;
useEffect를 사용하는데, 어떤 함수를 컴포넌트가 렌더링 될 때 단 한번만 실행하고 싶으면 의존성 배열을 [] 빈 상태로 넣으면 된다.
(3) 코드로 보는 의존성 배열 → 의존성 배열에 값이 있는 경우
빈 배열을 넣었을 때, 최초 렌더링 이후에는 useEffect가 실행되지 않다는 것을 위에서 배웠다. 그러면 이제 의존성 배열에 value 를 넣어보자. 배운게 맞다면, value는 state이고 우리가 input을 입력할 때마다 그 값이 변하게 되니 useEffect도 계속 실행이 되겠지?
// src/App.js
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
useEffect(() => {
console.log("hello useEffect");
}, [value]); // value를 넣음
return (
<div>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</div>
);
}
export default App;
의존성 배열에 [] 빈값으로 넣어 놓으면 어떤 값이 바뀌든 어떤 스테이트가 변해도 화면이 처음 로딩 될때만 동작한다.
만약 value가 변경 됐을 때 호출하고 싶으면 아래와 같이 적는다.
function App() {
const [value, setValue] = useState('')
useEffect(()=>{
console.log(`hello useEffect!' : ${value}`);
}, [value]);
컴포넌트가 나타났을 때 (렌더링 됐을 때 === 함수 컴포넌트가 실행 됐을 때) useEffect의 effect 함수가 실행되는 것은 배웠으니, 이제 컴포넌트가 사라졌을 때 무언가를 어떻게 실행하는지 알아보자. 이 과정을 클린 업 (clean up) 이라고 표현한다.
클린 업을 하는 방법은 간단하다. useEffect 안에서 return 을 해주고 이 부분에 실행 되길 원하는 함수를 넣으면 된다.
// src/App.js
import React, { useEffect } from "react";
const App = () => {
useEffect(()=>{
// 화면에 컴포넌트가 나타났을(mount) 때 실행하고자 하는 함수를 넣어주세요.
return ()=>{
// 화면에서 컴포넌트가 사라졌을(unmount) 때 실행하고자 하는 함수를 넣어주세요.
}
}, [])
return <div>hello react!</div>
};
export default App;
속세를 벗어나는 버튼을 만들었고 버튼을 누르면 useNavigate에 의해서 /todos로 이동하면서 속세 컴포넌트를 떠날 것 이다. 그러면서 화면에서 속세 컴포넌트가 사라질 것 이고, useEffect의 return 부분이 실행 될 것이다.
// src/SokSae.js
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const 속세 = () => {
const nav = useNavigate();
useEffect(() => {
return () => {
console.log(
"안녕히 계세요 여러분! 전 이 세상의 모든 굴레와 속박을 벗어 던지고 제 행복을 찾아 떠납니다! 여러분도 행복하세요~~!"
);
};
}, []);
return (
<button
onClick={() => {
nav("/todos");
}}
>
속세를 벗어나는 버튼
</button>
);
};
export default 속세;
/ 에서 /todos 잘 이동했고, 그 과정에서 clean up이 실행되었다.
-useEffect는 화면에 컴포넌트가 mount 또는 unmount 됐을 때 실행하고자 하는 함수를 제어하게 해주는 훅이다.
-의존성 배열을 통해 함수의 실행 조건을 제어할 수 있다.
-useEffect 에서 함수를 1번만 실행시키고자 할때는 의존성 배열을 빈 배열로 둔다.
DOM 요소에 접근할 수 있도록 하는 React Hook 이다. HTML과 javascript를 사용했을 때 우리는 특정 DOM을 선택하기 위해서 다음과 같이 했었다.
// (1) getElementById 이용
const divTag = document.getElementById('#myDiv');
// (2) querySelector 이용
const divTag2 = document.querySelector('#myDiv');
리액트에서도 DOM을 선택해야 할 상황이 있다. 예를 들면 화면이 렌더링 되자마자 특정 input 태그가 focusing이 돼야 하는 경우 등 말이다. 그럴 경우에 우리는 useRef hook을 사용할 수 있다.
useRef는 DOM요소에 접근할 수 있도록 한다. 이 말은 DOM요소를 어딘가에 이제 저장해야겠지? 어딘가에 저장하고 그거를 가져다 쓸 수 있게 하는 그 레퍼런스를 가져다 쓸 수 있게 하는 그런 방법들을 주는 것들이 useRef hook이 하는 역할이다.
useRef에서 ref는 Reference를 의미한다.
useRef hook은 다음과 같이 사용한다.
import "./App.css";
import { useRef } from "react";
function App() {
const ref = useRef("초기값");
console.log("ref", ref);
return (
<div>
<p>useRef에 대한 이야기에요.</p>
</div>
);
}
export default App;
콘솔을 확인해보면, ref에는 값이
useRef console찍어보면 키가 current이다.
변경도 가능하다.
import "./App.css";
import { useRef } from "react";
function App() {
const ref = useRef("초기값");
console.log("ref 1", ref);
ref.current = "바꾼 값";
console.log("ref 1", ref);
return (
<div>
<p>useRef에 대한 이야기에요.</p>
</div>
);
}
export default App;
콘솔은 다음과 같다.
(중요) 이렇게 설정된 ref 값은 컴포넌트가 계속해서 렌더링 되어도 unmount 전까지 값을 유지한다!
이러한 특징 때문에 useRef는 다음 2가지 용도로 사용이 된다.
1-1. state와 비슷한 역할을 한다. 다만 state는 변화가 일어나면 다시 렌더링이 일어난다. 내부 변수들은 초기화가 된다.
1-2. ref에 저장한 값은 렌더링을 일으키지 않는다. 즉, ref의 값 변화가 일어나도 렌더링으로 인해 내부 변수들이 초기화 되는 것을 막을 수 있다.
1-3. 컴포넌트가 100번 렌더링 → ref에 저장한 값은 유지돼요.
2. 정리하면
2-1. state는 리렌더링이 꼭 필요한 값을 다룰 때 쓰면 된다.
2-2. ref는 리렌더링을 발생시키지 않는 값을 저장할 때 사용한다.
2-1. 렌더링 되자마자 특정 input이 focusing 돼야 한다면 useRef를 사용하면 된다.
state와 ref의 차이점을 코드를 통해 살펴보기로 하자.
plusStateCountButtonHandler와, plusRefCountButtonHandler는 다르게 동작한다.
state는 변경되면 렌더링이 되고, ref는 변경되면 렌더링이 안된다는걸 다시 한번 기억하자.
App.jsx
import "./App.css";
import { useRef, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const plusStateCountButtonHandler = () => {
setCount(count + 1);
};
const plusRefCountButtonHandler = () => {
countRef.current++;
};
return (
<>
<div>
state 영역입니다. {count} <br />
<button onClick={plusStateCountButtonHandler}>state 증가</button>
</div>
<div>
ref 영역입니다. {countRef.current} <br />
<button onClick={plusRefCountButtonHandler}>ref 증가</button>
</div>
</>
);
}
export default App;
근데, 내부 변수는 그냥 let 키워드로 선언해서 변수 사용하면 안되나?
let 키워드를 사용하게 되면 렌더링 시, 다시 변수가 초기화가 된다. 함수라서 그렇다.
즉, 리렌더링이 된다 = 함수가 다시 호출된다 = 내부 변수가 다시 쫙 초기화된다.
태그에는 ref라는 속성이 있다. 이걸 통해 우리는 해당 DOM 요소로 접근할 수 있다.
import "./App.css";
function App() {
return (
<>
<div>
아이디 : <input type="text" />
</div>
<div>
비밀번호 : <input type="password" />
</div>
</>
);
}
export default App;
간단한 아이디와 비밀번호를 입력받는 구조이다.
그런데, 화면이 렌더링 되고나면 아이디에 자동 포커싱 되게 할 순 없을까?
import { useEffect, useRef } from "react";
import "./App.css";
function App() {
const idRef = useRef("");
// 렌더링이 될 때
useEffect(() => {
idRef.current.focus();
}, []);
return (
<>
<div>
아이디 : <input type="text" ref={idRef} />
</div>
<div>
비밀번호 : <input type="password" />
</div>
</>
);
}
export default App;
화면이 렌더링 되자마자 바로 focus가 들어간다.
위 코드에서 아이디가 10자리 입력되면 자동으로 비밀번호 필드로 이동하도록 하기 위해서는 어떻게 할까?
import { useEffect, useRef, useState } from "react";
import "./App.css";
function App() {
const idRef = useRef("");
const pwRef = useRef("");
const [id, setId] = useState("");
const onIdChangeHandler = (event) => {
setId(event.target.value);
};
// 렌더링이 될 때
useEffect(() => {
idRef.current.focus();
}, []);
// 왜 useEffect 안에 놓았을까요?
useEffect(() => {
if (id.length >= 10) {
pwRef.current.focus();
}
}, [id]);
return (
<>
<div>
아이디 :
<input
type="text"
ref={idRef}
value={id}
onChange={onIdChangeHandler}
/>
</div>
<div>
비밀번호 : <input type="password" ref={pwRef} />
</div>
</>
);
}
export default App;
우리는 일반적으로 부모컴포넌트 → 자식 컴포넌트로 데이터를 전달해 줄 때 어떻게 했었지??
props
를 사용했다. 그러나 부모 → 자식 → 그 자식 → 그자식의 자식 이렇게 너무 깊어지게 되면 prop drilling 현상이 일어났었다.
prop drilling의 문제점은
1)깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악이 어렵다.
2)어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들어져서 대처가 늦을 수 밖에 없다.
그래서 등장한 것이 바로 react context API라는 것다. useContext hook을 통해 우리는 쉽게 전역 데이터를 관리할 수 있게 되었다. 후에 배울 Redux와 항상 비교되곤 한다.
- createContext : context 생성
- Consumer : context 변화 감지
- Provider : context 전달(to 하위 컴포넌트)
구조는 다음과 같다.
App.jsx
import "./App.css";
import GrandFather from "./components/GrandFather";
export function App() {
return <GrandFather />;
}
export default App;
GrandFather.jsx
import React from "react";
import Father from "./Father";
function GrandFather() {
const houseName = "스파르타";
const pocketMoney = 10000;
return <Father houseName={houseName} pocketMoney={pocketMoney} />;
}
export default GrandFather;
Father.jsx
import React from "react";
import Child from "./Child";
function Father({ houseName, pocketMoney }) {
return <Child houseName={houseName} pocketMoney={pocketMoney} />;
}
export default Father;
Child.jsx
import React from "react";
function Child({ houseName, pocketMoney }) {
const stressedWord = {
color: "red",
fontWeight: "900",
};
return (
<div>
나는 이 집안의 막내에요.
<br />
할아버지가 우리 집 이름은 <span style={stressedWord}>{houseName}</span>
라고 하셨어요.
<br />
게다가 용돈도 <span style={stressedWord}>{pocketMoney}</span>원만큼이나
주셨답니다.
</div>
);
}
export default Child;
결과 화면
GrandFather 컴포넌트는 Child 컴포넌트에게 houseName과 pocketMoney를 전달해주기 위해 Father 컴포넌트를 거칠 수 밖에 없었다. 단적인 예시였지만 중간 컴포넌트가 100개라면 엄청나게 비효율이겠지? 이제 useContext hook을 적용해보자.
import { createContext } from "react";
// 여기서 null이 의미하는 것은?
export const FamilyContext = createContext(null);
#2. GrandFather.jsx 수정
import React from "react";
import Father from "./Father";
import { FamilyContext } from "../context/FamilyContext";
function GrandFather() {
const houseName = "스파르타";
const pocketMoney = 10000;
return (
<FamilyContext.Provider value={{ houseName, pocketMoney }}>
<Father />
</FamilyContext.Provider>
);
}
export default GrandFather;
#3. Father.jsx 수정(props를 제거!)
import React from "react";
import Child from "./Child";
function Father() {
return <Child />;
}
export default Father;
#4. Child.jsx 수정
import React, { useContext } from "react";
import { FamilyContext } from "../context/FamilyContext";
function Child({ houseName, pocketMoney }) {
const stressedWord = {
color: "red",
fontWeight: "900",
};
const data = useContext(FamilyContext);
console.log("data", data);
return (
<div>
나는 이 집안의 막내에요.
<br />
할아버지가 우리 집 이름은 <span style={stressedWord}>{data.houseName}</span>
라고 하셨어요.
<br />
게다가 용돈도 <span style={stressedWord}>{data.pocketMoney}</span>원만큼이나
주셨답니다.
</div>
);
}
export default Child;
Child.jsx에서 console.log를 찍어보면 어떻게 나올까?
GrandFather → Context(중앙 관리소) → Child 순서로 잘 전달이 됐다.
이제 이 object를 이용해서 뿌려보자.
<span style={stressedWord}>{data.houseName}</span>
<span style={stressedWord}>{data.pocketMoney}</span>
useContext를 사용할 때, Provider에서 제공한 value가 달라진다면 useContext를 사용하고 있는 모든 컴포넌트가 리렌더링 된다. 따라서 value 부분을 항상 신경써줘야 한다!
이후에 배우게 될 메모이제이션이 그 키가 될 것이다.
useContext는 React에서 제공하는 기본적인 context라고 볼 수 있다. context 라는 말은 많은 프로그래밍 언어에서 등장하는 개념. 전역적으로 사용되는 어떠한 것을 표현할 때 context 라는 말을 보통 쓴다.
Redux와 context는 항상 비교가 된다.
context 를 쓰면 전역데이터를 관리하고 거기에 자식 그 자식의 자식 등등 다 접근할 수 있다. Prop drilling현상 일어나지 않아도 된다.
주의해야 될 사항이 있다.
context가 좋다고 마구잡이로 사용해서는 안 된다. 랜더링 문제가 있다. Provider의 value (내가 해본 연습에서는 Grand Father가 Provider) 가 변경되면 아래의 모든 컴포넌트 value 다 달라지고 리랜더링됨으로 굉장히 비효율적. 그래서 value부분 항상 신경써줘야 하는데 그 대안이 메모리제이션이 있다.
(1-1)컴포넌트에서 state가 바뀌었을 때
(1-2)컴포넌트가 내려받은 props가 변경 되었을 때
(1-3)부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두
지금까지 useState, useEffect, useRef, useContext 등 많은 훅을 배웠다. 그러면서 자연스럽게 렌더링에 대해 자주 들었었는데,
리액트에서 리렌더링이 빈번하게, 자주 일어난다는 것은 좋지 않다고 한다. 비용이 발생하는 것은 최대한 줄여야 한다. 이런 작업을 최적화(Optimization)라고 부른다. 리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법이 바로
리-렌더링의 발생 조건 중 3번째 경우. 즉, 부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다는 것은 그림으로 보면
자녀 컴포넌트의 입장에서는 “나는 바뀐게 없는데 왜 다시 렌더링 돼야하지?”라고 할 수 있다. 이 부분을 돕는 도구가 바로 React.memo 이다.
아래와 같은 예제 코드를 만들어보자
디렉토리 구성
결과물
App.jsx
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
function App() {
console.log("App 컴포넌트가 렌더링되었습니다!");
const [count, setCount] = useState(0);
// 1을 증가시키는 함수
const onPlusButtonClickHandler = () => {
setCount(count + 1);
};
// 1을 감소시키는 함수
const onMinusButtonClickHandler = () => {
setCount(count - 1);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 />
<Box2 />
<Box3 />
</div>
</>
);
}
export default App;
Box1.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box1() {
console.log("Box1이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
}
export default Box1;
Box2.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box2() {
console.log("Box2가 렌더링되었습니다.");
return <div style={boxStyle}>Box2</div>;
}
export default Box2;
Box3.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#c491be",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box3() {
console.log("Box3가 렌더링되었습니다.");
return <div style={boxStyle}>Box3</div>;
}
export default Box3;
plus 버튼 또는 minus 버튼을 누르면
chrome 브라우저
모든 하위 컴포넌트가 리렌더링 되고 있다. 실제로 변한 것은 부모컴포넌트, App.jsx 뿐인데도 말이다.
정말 간단히 React.memo를 이용해서 컴포넌트를 메모리에 저장해두고 필요할 때 가져다 써보자. 이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않는다. 이것을 컴포넌트 memoization 이라고 한다.
React.memo에서는 꼭 임포트를 하지 않아도 된다.
import { memo } from 'react'; // 이렇게 할 수도 있지만
React.memo로 많이 쓴다.
우리는 정말 간단히 React.memo를 이용해서 컴포넌트를 메모리에 저장해두고 필요할 때 갖다 쓰게 된다. 여기서 컴포넌트를 메모리에 저장-> 임시적으로 저장을 하는 것은 캐싱이라고 말한다.
Box1.jsx, Box2.jsx, Box3.jsx 모두 동일
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
자, 그러면 최초 렌더링 이외에는 App.jsx 컴포넌트의 state가 변경되더라도 자식 컴포넌트들은 렌더링이 되지 않는다. App.jsx 컴포넌트만 렌더링이 되었다.
React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)한다.
Box1이 만일, count를 초기화 해 주는 코드라고 가정해보자.
App.jsx
...
// count를 초기화해주는 함수
const initCount = () => {
setCount(0);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 initCount={initCount} />
<Box2 />
<Box3 />
</div>
</>
);
}
...
Box1.jsx
...
function Box1({ initCount }) {
console.log("Box1이 렌더링되었습니다.");
const onInitButtonClickHandler = () => {
initCount();
};
return (
<div style={boxStyle}>
<button onClick={onInitButtonClickHandler}>초기화</button>
</div>
);
}
...
const onInitButtonClickHandler = () => {
initCount();
};
코드가 다시 만들어지기 때문.
initCount를 Box1에 props로 넣어주고 console찍으면 Box1도 count될때마다 콘솔에 찍혀.
그건 함수형 컴포넌트를 사용했기 때문. InitCount는 근데 바뀐게 없어. 초기화 버튼이잖아. 숫자 증가감소한다고 바꼈어?? 아니. 그럼 왜?
불변성 기억나? 리액트는 state가 바뀌었는지 안 바뀌었는지 알기 위해서는 callstack주소값으로 판단. 함수나 객체같은 것들은 꼭 불변성을 유지하고 바꾸어줘야한다.
함수도 자바스크립트에서는 객체다. 이 함수도 규모가 조금 있어서 메모리에 저장 될때 직접적으로 저장되는 게 아니라 별도의 공간 heap stack(?)에 저장. 그 별도의 공간을 바라보고 있는 주소값을 저장. 근데 함수형 컴포넌트가 다시 리랜더링 되면서 이 함수들이 다시 만들어질 때 이전에 있었던 그 함수는 그대로 있고 이 initCount를 새로운 공간에 저장을 하면서 그 새로운 주소를 다시 리턴해준다. 결국 이 initCount는 새로운 주소값을 가지게 된다. 그렇기 떄문에 이 props로 내려줄 떄 새로운 주소값을 주기 때문에 Box1입장은 아 props가 바뀌었구나 하고 다시 랜더링이 된다. 이러한 상황들 떄문에 useCallback이라는 함수를 통해서 함수 자체를 메모이제이션 하는 방법이 필요하다.
그렇기 때문에,
const onInitButtonClickHandler = () => {
initCount();
};
이 함수를 메모리 공간에 저장해 놓고, 특정 조건이 아닌 경우엔 변경되지 않도록 해야한다.
# useCallback hook의 적용
App.jsx
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
자, 이렇게 하고 나니 우리가 원하는 결과대로 Box1.jsx 컴포넌트는 리렌더링이 안되고 있다.
count를 초기화 할 때, 콘솔을 찍어보자.
App.jsx
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
…
현재 count가 7일 때 [초기화] 버튼을 누를 꺼니까 콘솔에는 7에서 0으로 가 보여야지.
하지만 실제론 어떨까?
이런 현상이 발생하는 이유는, useCallback이 count가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문이다. 이 때문에 우리는 dependency array가 필요하다.
기존 코드의 dependency array에 count를 넣으면, count가 변경 될 때 마다 새롭게 함수를 할당할 것이다.
App.jsx
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]);
...
잘 나오는 것을 볼 수 있다.
일단, 여기서 말하는 memo는 memoization을 뜻한다. 기억한다는 말. 어떤 것을 기억한다는 말일까?
동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 한다고 볼 수 있겠지? 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장한다. 이렇게 하면 필요할 때 마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있다. 보통 이러한 기법을 “캐싱을 한다.” 라고 표현하기도 한다.
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
dependency Array의 값이 변경 될 때만 “반환할_함수()”가 호출된다.
그 외의 경우에는 memoization 해놨던 값을 가져오기만 하면 된다.
HeavyComponent 안에서는 const value = heavyWork() 를 통해서 value값을 세팅해주고 있다. 만약 heavyWork가 엄청나게 무거운 작업이라면 다른 state가 바뀔 때 마다 계속해서 호출이 되겠지. 하지만 useMemo()로 감싸주게 되면 그럴 걱정이 없다.
App.jsx
import "./App.css";
import HeavyComponent from "./components/HeavyComponent";
function App() {
const navStyleObj = {
backgroundColor: "yellow",
marginBottom: "30px",
};
const footerStyleObj = {
backgroundColor: "green",
marginTop: "30px",
};
return (
<>
<nav style={navStyleObj}>네비게이션 바</nav>
<HeavyComponent />
<footer style={footerStyleObj}>푸터 영역이에요</footer>
</>
);
}
export default App;
components > HeavyComponent.jsx
import React, { useState, useMemo } from "react";
function HeavyButton() {
const [count, setCount] = useState(0);
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100;
};
// CASE 1 : useMemo를 사용하지 않았을 때
const value = heavyWork();
// CASE 2 : useMemo를 사용했을 때
// const value = useMemo(() => heavyWork(), []);
return (
<>
<p>나는 {value}을 가져오는 엄청 무거운 작업을 하는 컴포넌트야!</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
누르면 아래 count가 올라가요!
</button>
<br />
{count}
</>
);
}
export default HeavyButton;
Case1과 2는 랜더링 속도 자체가 다르다.
만일, 아래와 같은 코드가 있다면 어떨까?
import React, { useEffect, useState } from "react";
function ObjectComponent() {
const [isAlive, setIsAlive] = useState(true);
const [uselessCount, setUselessCount] = useState(0);
const me = {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
return (
<>
<div>
내 이름은 {me.name}이구, 나이는 {me.age}야!
</div>
<br />
<div>
<button
onClick={() => {
setIsAlive(!isAlive);
}}
>
누르면 살았다가 죽었다가 해요
</button>
<br />
생존여부 : {me.isAlive}
</div>
<hr />
필요없는 숫자 영역이에요!
<br />
{uselessCount}
<br />
<button
onClick={() => {
setUselessCount(uselessCount + 1);
}}
>
누르면 숫자가 올라가요
</button>
</>
);
}
export default ObjectComponent;
useEffect hook을 이용해서 me의 정보가 바뀌었을 때만 발동되게끔 dependency array를 넣어놨다. 그렇지만 count를 증가하는 button을 눌러보면 계속 log가 찍히는 것을 볼 수 있다.
왜 그럴까?
불변성과 관련이 깊다.
위 예제에서 버튼이 선택돼서 uselessCount state가 바뀌게 되면 → 리렌더링이 된다 → 컴포넌트 함수가 새로 호출된다 → me 객체도 다시 할당한다(이 때, 다른 메모리 주소값을 할당받는다.) → useEffect의 dependency array에 의해 me 객체가 바뀌었는지 확인해봐야 하는데 → 이전 것과 모양은 같은데 주소가 다르다 → 리액트 입장에서는 me가 바뀌었구나 인식하고 useEffect 내부 로직이 호출된다.
조금 더 예시를 들자면,
// 이건 일치한다.
const a = 1;
const b = 1;
console.log(a === b); // true
// 하지만 이건 다르다.
const me = {
name: "ted chang",
age: 21,
};
const you = {
name: "ted chang",
age: 21,
};
console.log(me === you); // false
이런 상황을 해결하기 위해서 또한 useMemo를 활용할 수 있다.
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
useMemo()만 이렇게 써주면, uselessCount가 아무리 증가되어도 영향이 없다.
useMemo를 남발하게 되면 별도의 메모리 확보를 너무나 많이 하게 되기 때문에 오히려 성능이 악화될 수 있다. 필요할 때만 쓰자.