[ React Hooks - useContext(Context API) ]
▶ useContext 소개
react context의 필요성
일반적으로 부모컴포넌트에서 자식컴포넌트로 데이터를 전달해줄때 props를 사용
그러나 너무 깊어지게 되면 prop drilling 현상이 일어남
prop drilling의 문제점은
- 깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악이 어려움
- 어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들어지니 대처가 늦을 수 밖에 없음
이 문제를 해결하기 위해 나온것이 react context API 라는 것
useContext hook을 통해 쉽게 전역 데이터를 관리할 수 있음
context API 필수 개념
- createContext : context 생성
- Consumer : context 변화 감지
- Provider : context 전달(to 하위 컴포넌트)
▶ 코드로 구현해보기
//FamilyContext.js
import { createContext } from "react";
export const FamilyContext = createContext(null);
//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;
//Father.jsx
import React from "react";
import Child from "./Child";
function Father() {
return <Child />;
}
export default Father;
//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;
GrandFather → Context(중앙 관리소) → Child 순서로 전달
▶ 주의해야 할 사항
렌더링 문제
useContext를 사용할 때 Provider에서 제공한 value가 달라진다면 useContext를 사용하고 있는 모든 컴포넌트가 리렌더링 됨. 따라서 value 부분을 항상 신경써줘야 함
[ React Hooks - 최적화(React.memo, useCallback, useMemo) ]
▶ 시작하기에 앞서
리-렌더링의 발생 조건
- 컴포넌트에서 state가 바뀌었을 때
- 컴포넌트가 내려받은 props가 변경되었을 때
- 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두
최적화
리렌더링이 빈번하게 자주 일어난다는 것은 그렇게 좋은 소식은 아님. 비용이 발생하는 것은 최대한 줄여야 함
이런 작업을 최적화(Optimization)라고 부름. 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적 방법이 있음
- memo(React.memo) : 컴포넌트를 캐싱
- useCallback : 함수를 캐싱
- useMemo : 값을 캐싱
▶ memo(React.memo)
memo
리-렌더링의 발생 조건 중 부모 컴포넌트가 리렌더링 되면 자식컴포넌트는 모두 리렌더링 된다는 것은
- 1번 컴포넌트가 리렌더링 된 경우 2~7번이 모두 리렌더링 됨
- 4번 컴포넌트가 리렌더링 된 경우 6, 7번이 모두 리렌더링 됨
자녀 컴포넌트의 입장에서는 바뀐것이 없는데 다시 렌더링되는것. 이 부분을 돕는 도구가 React.memo
memo를 통한 문제 해결
React.memo를 이용해서 컴포넌트를 메모리에 저장(캐싱)해두고 필요할 때 갖다 쓰게 됨
이렇게 하면 부모 컴포넌트의 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트는 리렌더링 되지 않움
이것을 컴포넌트 memoization 이라고 함
Box1.jsx, Box2.jsx, Box3.jsx 모두 동일
//App.js
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;
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
내보낼때 export default로 그냥 빼는 것이 아니라 React.memo로 해주면 됨
그러면 최초 렌더링 이외에는 App.jsx 컴포넌트의 state가 변경되더라도 자식 컴포넌트들은 렌더링이 되지 않음
App.jsx 컴포넌트만 렌더링이 됨
▶ useCallback
useCallback
useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)
예제로 통해 보는 useCallback의 필요성
//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>
);
}
...
+버튼이나 -버튼을 누를 때 그리고 초기화 버튼을 누를 때 모두 App 컴포넌트와 Box1 컴포넌트가 리렌더링 됨
React.memo를 통해서 Box1.jsx는 메모이제이션을 했는데도 리렌더링이 됨
이유는 함수형 컴포넌트를 사용하기 때문이고 App.jsx가 리렌더링 되면서 코드가 다시 만들어지기 때문
const onInitButtonClickHandler = () => {
initCount();
};
JS에서는 함수도 객체의 한 종류. 따라서 모양은 같더라도 다시 만들어지면 그 주소값이 달라지고 이에 따라 하위 컴포넌트인 Box1.jsx는 props가 변경됐다고 인식
useCallback을 사용을 통한 함수 메모이제이션
함수를 메모리 공간에 저장해놓고 특정 조건이 아닌 경우엔 변경되지 않도록 해야함
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
조금더 나아가기
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
...
이런 현상이 발생하는 이유는, useCallback이 count가 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문
이 때문에 dependency array가 필요함
기존 코드의 dependency array에 count를 넣으면 count가 변경 될 때 마다 새롭게 함수를 할당함
▶ useMemo
useMemo
여기서 말하는 memo는 memoization을 뜻함. 동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 한다고 볼 수 있음. 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장함. 이렇게 하면 필요할 때 마다 다시 함수를 호출해서 계산하는게 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있음. 보통 이러한 기법을 캐싱을 한다고 표현
사용방법
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
dependency Array의 값이 변경 될 때만 반환할_함수()가 호출
그 외의 경우에는 memoization 해놨던 값을 가져오기만 함
useMemo 적용해보기
HeavyComponent 안에서는 const value = heavyWork() 를 통해서 value값을 세팅해주고 있음
만약 heavyWork가 엄청나게 무거운 작업이라면 다른 state가 바뀔 때 마다 계속해서 호출이 됨
하지만 useMemo()로 감싸주게 되면 그럴 걱정이 없음
//App.js
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;
useMemo 적용해보기
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 내부 로직이 호출됨
이런 상황을 해결하기 위해서 useMemo를 활용할 수 있음
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
useMemo()만 이렇게 쓰면 uselessCount가 아무리 증가돼도 영향이 없게 됨
▶ 주의해야 할 사항
useMemo를 남발하게 되면 별도의 메모리 확보를 너무나 많이 하게 되기 때문에 오히려 성능이 악화될 수 있음
필요할 때만 써야함
[ LifeCycle - 클래스형 컴포넌트에서의 라이프사이클 이해하기 ]
▶ 생명주기(LifeCycle) 소개
리액트 생명주기
리액트 컴포넌트는 각각 Mount → Update → Unmount의 과정을 거침. 사람처럼 태어나고 변화하고 죽는 것
리액트 생명주기(라이프사이클)란 컴포넌트 중심 라이브러리의 집합체라고 보면 됨
모든 컴포넌트에는 각각의 생명주기가 존재하고 각 생명주기에 맞는 메서드들이 있음
생명주기
▶ Mount
컴포넌트가 생성될 때를 말함
- constructor
- 컴포넌트가 맨 처음 만들어 질 때 호출
- 생성자
- getDerivedStateFromProps
- 부모 컴포넌트로부터 props를 전달받을 때, state에 값을 일치시키는 역할을 하는 메서드
- 마운트 될 때, 업데이트(리렌더링) 될 때도 호출
- render
- 최초 mount가 준비완료 되면 호출되는, 즉 렌더링 하는 메서드
- 컴포넌트를 DOM에 마운트하기 위해 사용
- componentDidMount
- 컴포넌트가 브라우저에 표시가 된 후 호출되는 메서드
▶ Update
컴포넌트가 갱신될 때를 말함
- getDerivedStateFromProps
- Mount 과정에서도 동일하게 호출되었던 메서드.
- 부모 컴포넌트로부터 props를 전달받을 때, state에 값을 일치시키는 역할을 하는 메서드
- shouldComponentUpdate
- 리렌더링 여부 판단(함수 호출 결과 : true / false)
- true인 경우 : 리렌더링 진행
- false인 경우 : 리렌더링 하지 않음
- 함수형 컴포넌트에서 memo, useMemo, useCallback이 역할을 대신한다.
- 리렌더링 여부 판단(함수 호출 결과 : true / false)
- render
- 변경사항 반영이 다 되어 준비완료 되면 호출되는, 즉 렌더링 하는 메서드
- 컴포넌트를 DOM에 마운트하기 위해 사용
- getSnapshotBeforeUpdate
- 컴포넌트에 변화가 일어나기 직전 DOM의 상태를 저장
- componentDidUpdate 함수에서 사용하기 위한 스냅샷 형태의 데이터
- componentDidUpdate
- 컴포넌트 업데이트 작업 완료 후 호출
▶ Unmount
컴포넌트가 DOM에서 제거되는 시점을 말함
- mponentWillUnmount
- 컴포넌트가 사라지기 전 호출되는 메서드
- useEffect의 return과 동일
[ DOM과 Virtual DOM ]
▶ 개요
가상돔
리액트(react.js)나 뷰(Vue.js)는 가상돔(Virtual DOM)을 사용해서 원하는 화면을 브라우저에 그려줌
자체적으로 상당히 효율적인 알고리즘을 사용해서 그려주기 때문에 그 속도가 어마어마함
▶ DOM
DOM
브라우저를 돌아다니다 보면 수 많은 컴포넌트로 구성된 웹페이지들을 보게 됨
그 페이지를 문서(document)라고 함. 페이지를 이루는 컴포넌트를 엘리먼트(element)라고 해요.
DOM은 이 엘리먼트를 tree형태(= DOM TREE)로 표현한 것
트리의 요소 하나하나를 ‘노드’라고 부름. 각각의 ‘노드’는 해당 노드에 접근과 제어(DOM 조작)를 할 수 있는 API를 제공
API는 단순히 HTML 요소에 접근해서 수정할 수 있는 함수 정도로 이해하면 됨
DOM 사용 예시
// id가 demo인 녀석을 찾아, 'Hello World!'를 대입해줘.
document.getElementById("demo").innerHTML = "Hello World!";
// p 태그들을 모두 가져와서 element 변수에 저장해줘
const element = document.getElementsByTagName("p");
// 클래스 이름이 intro인 모든 요소를 가져와서 x 변수에 저장해줘
const x = document.getElementsByClassName("intro");
form validation도 이런식으로 가능함
function validateForm() {
let x = document.forms["myForm"]["fname"].value;
if (x == "") {
alert("Name must be filled out");
return false;
}
}
<form name="myForm" action="/action_page.php" onsubmit="return validateForm()" method="post">
Name: <input type="text" name="fname">
<input type="submit" value="Submit">
</form>
▶ 가상DOM(Virtual DOM)
가상DOM
리액트는 가상DOM을 이용해서 실제DOM을 변경하는 작업을 상당히 효율적으로 수행
가상DOM은 실제 DOM과 구조가 완벽히 동일한 복사본 형태라고 보면됨
실제 DOM은 아니지만 객체(object) 형태로 메모리에 저장되기 때문에 실제 DOM을 조작하는 것 보다 훨씬 더 빠르게 조작을 수행할 수 있음(실제 DOM을 조작하는 것보다 메모리상에 올라와있는 JS 객체를 변경하는 작업이 훨씬 더 가벼움)
DOM 조작 과정
만일 인스타그램의 좋아요 버튼을 누른다면 화면이 바뀌어야 함
빨간색 하트에 해당되는 엘리먼트 DOM 요소가 갱신돼야 한다는 것. 즉 DOM을 조작해야 한다는 의미입니다.
- 이 과정에서 리액트는 항상 2가지 버전의 가상DOM을 가지고 있음
- 화면이 갱신되기 전 구조가 담겨있는 가상DOM 객체
- 화면 갱신 후 보여야 할 가상 DOM 객체
- diffing
- state가 변경되면 2번에서 생성된 가상돔과 1번에서 이미 갖고있었던 가상돔을 비교해서 어느 부분(엘리먼트)에서 변화가 일어났는지를 상당히 빠르게 파악함
- 재조정(reconciliation)
- 파악이 다 끝나면 변경이 일어난 그 부분만 실제 DOM에 적용시켜줌. 적용시킬 때는 한건 한건 적용시키는 것이 아니라 변경사항을 모두 모아 한 번만 적용을 시켜요(Batch Update)
- Batch Update는 변경된 모든 엘리먼트를 한꺼번에 반영할 수 있는 방법
- 클릭 한번은 5개의 엘리먼트가 바뀐다면 실제 돔 5번의 화면 갱신, 가상돔은 Batch Update로 인해 단한번만 갱신 필요
- Batch Update는 변경된 모든 엘리먼트를 한꺼번에 반영할 수 있는 방법
- 파악이 다 끝나면 변경이 일어난 그 부분만 실제 DOM에 적용시켜줌. 적용시킬 때는 한건 한건 적용시키는 것이 아니라 변경사항을 모두 모아 한 번만 적용을 시켜요(Batch Update)
'항해99' 카테고리의 다른 글
리액트 숙련주차4 (0) | 2023.06.28 |
---|---|
리액트 숙련주차 3 (0) | 2023.06.27 |
리액트 숙련주차 1 (0) | 2023.06.27 |
리액트 입문주차 3 (0) | 2023.06.23 |
리액트 입문주차 2 (0) | 2023.06.23 |
댓글