2-3 useState
1. useState란?
const [state, setState] = useState(초기값);
- 가장 기본적인 hook
- 함수형 컴포넌트 내에서 가변적인 상태를 갖게 함
- 카운터, Todolist
2. 함수형 업데이트
// 기존에 우리가 사용하던 방식
setState(number + 1);
// 함수형 업데이트
setState(() => {});
// 현재 number의 값을 가져와서 그 값에 +1을 더하여 반환한 것 입니다.
setState((currentNumber)=>{ return currentNumber + 1 });
- 차이점 : 배치 업데이트 (state 파악하는 방법)
- 함수형은 동시에 명령을 내리면 명령을 모아 순차적으로 각각 한번씩 실행해줌
- 배치(batch) 업데이트 : 명령을 하나로 모아 최종적으로 한번만 실행시킴
- 불필요한 리렌더링을 방지하고, 렌더링을 최적화하기 위해 리액트에서 업데이트하는 방식
- 즉, useState의 업데이트 방식은 2가지가 있으며, 원시데이터가 아닌 데이터를 변경할 때에는 불변성을 유지해야 한다.
2-4 useEffect
1. useEffect란?
useEffect(() => { ~실행하고 싶은 콜백 함수~ }, [의존성 배열] );
- 컴포넌트가 mount, unmout 되었을 때 실행하고자 하는 함수를 제어하게 해주는 훅
- 컴포넌트가 렌더링 될 때 마다 특정한 작업을 수행해야 할 때 설정하는 훅
- 하지만 렌더링 뙬 때 마다 수행되므로, 예를 들어 input이 입력될때마다 수행되는 등의 문제가 발생할 수 있음
- 의존성 배열로 해결
2. 의존성 배열
- 의존성 배열(dependency array)이란?
-> 이 배열에 값을 넣으면 그 값이 바뀔 때만 useEffect를 실행할게
// useEffect의 두번째 인자가 의존성 배열이 들어가는 곳 입니다.
useEffect(()=>{
// 실행하고 싶은 함수
}, [의존성배열])
- 빈 배열이면? 어떤 값이 바뀌어도 useEffect는 처음 이외에 수행되지 않음
* 처음 두 번 출력되는 것을 막으려면? index.js에서 React.StrictMode 없애기
3. clean up
const App = () => {
useEffect(()=>{
// 화면에 컴포넌트가 나타났을(mount) 때 실행하고자 하는 함수를 넣어주세요.
return ()=>{
// 화면에서 컴포넌트가 사라졌을(unmount) 때 실행하고자 하는 함수를 넣어주세요.
}
}, [])
return <div>hello react!</div>
};
return ()=> { ~ }
- 컴포넌트가 사라졌을 때 무언가 실행하는 과정
- useEffect의 콜백 함수 내부에 함수 형태로 리턴문 작성
2-5 useRef
1. useRef란?
const ref = useRef("초기값")
- DOM 요소에 접근할 수 있도록 하는 React Hook
- 객체 형태라 current 키로 접근/변경 가능 -> ref.current
- 설정된 ref값은 컴포넌트가 렌더링되어도 unmount 전까지 계속 유지됨
2. 용도
저장공간으로 사용
- state와 비슷한 역할을 하지만, state는 변화가 일어나면 렌더링이 일어나고 내부 변수들이 초기화됨
- ref는 렌더링을 일으키지 않으므로, 내부 변수들이 초기화 되지 않음
- 즉 리렌더링이 꼭 필요한 값을 다룰 때는 state, 필요없을 때는 ref 사용하여 저장
- 예제 코드 실행 시, state값은 렌더링되지만 ref값은 렌더링되지 않음
- let 키워드와의 차이점 : let 또한 렌더링 시(함수가 다시 호출 되므로) 초기화 됨
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;
DOM 요소에 접근하는 방식으로 사용
- 예를 들어 네이버나 구글에서 렌더링 되자마자 특정 input이 focusing 되어야 하는 경우가 있을 때 사용
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;
* 아이디가 10자리 입력되면 자동으로 비밀번호 필드로 이동하는 코드
import { useEffect, useRef, useState } from "react";
import "./App.css";
function App() {
const idRef = useRef("");
const pwRef = useRef("");
const [id, setId] = useState("");
// 처음 렌더링이 될 때만
useEffect(() => {
idRef.current.focus();
}, []);
// 의존성 배열에 id값을 넣어줌 -> id 값이 바뀔 때 마다 실행
useEffect(() => {
if (id.length >= 10) pwRef.current.focus();
}, [id]);
return (
<>
<div>
아이디 :
<input
type="text"
ref={idRef}
value={id}
onChange={(event) => setId(event.target.value)}
/>
</div>
<div>
비밀번호 : <input type="password" ref={pwRef} />
</div>
</>
);
}
export default App;
2-6 useContext
1. useContext란?
- 전역 데이터를 관리는 API
- props와 prop drilling : 깊이가 너무 깊어지면 어떤 컴포넌트로 왔는지 파악하기 힘듦
2. 필수 개념
- createContext : context 생성
- Consumer : context 변화 감지
- Provider : context 전달(to 하위 컴포넌트)
3. 사용 예
1. context 폴더 생성 후, FamilyContext.js 생성
import { createContext } from "react";
// 여기서 null이 의미하는 것은 무엇일까요?
export const FamilyContext = createContext(null);
2. GrandFather.jsx 수정
- FamilyContext import 하고, context를 주입할 컴포넌트 Father를 감싸 value로 객체를 전달
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 제거
4. Child.jsx 수정
- data 라는 변수에 useContext로 FamilyContext 받아와 저장
- data.[context 이름]으로 값 사용
import React, { useContext } from "react";
import { FamilyContext } from "../context/FamilyContext";
function Child() {
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;
3. 주의사항
- Provider에서 제공한 value가 달라지면, useContext를 사용하는 모든 컴포넌트가 리렌더링 됨
- 항상 value를 신경써주어야 함
- 메모이제이션 *
2-7 컴포넌트 최적화 - React.memo
1. 리-렌더링의 발생 조건
1. State가 바뀌었을 때
2. props가 변경되었을 때
3. 부모 컴포넌트가 리렌더링 되었을 때
2. 최적화
- 렌더링이 많이 일어나는 것은 좋지 않음 -> 비용(cost)가 많이 든다
- 이를 줄이는 작업을 최적화(Optimization)라고 함
- memo(React.memo) : 컴포넌트를 캐싱
- useCallback : 함수를 캐싱
- useMemo : 값을 캐싱
3. memo (React.memo)
- memo란? : 3번 조건처럼 부모가 렌더링될 때 자식이 리렌더링 되지 않아도 되는 경우 존재
- 이를 해결하는 도구
예제 : 부모 컴포넌트에서 state인 count가 변경될 때, 자식인 박스 컴포넌트들이 함께 리렌더링 되는 상황
import React, { useState } from "react";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
function Box1() {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
console.log("Box1이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
}
function Box2() {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
console.log("Box2가 렌더링되었습니다.");
return <div style={boxStyle}>Box2</div>;
}
function App() {
console.log("App 컴포넌트가 렌더링되었습니다!");
const [count, setCount] = useState(0);
const onPlusButtonClickHandler = () => {
setCount(count + 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 />
</div>
</>
);
}
export default App;
- 각 컴포넌트에 console.log로 박스가 렌더링되었다는 문구를 출력함 -> 이는 렌더링 될 때 마다 실행됨
- 실행 결과 : App.jsx컴포넌트에 존재하는 + 나 - 버튼을 눌렀는데 모든 자식 컴포넌트들이 다시 리렌더링 되어 console.log를 실행하는 모습
예제 문제 해결 - memo 사용
- React.memo를 이용하여 컴포넌트를 메모리에 저장해두고(캐싱), 필요할 때 갖다 쓰는 방식
- 부모 컴포넌트의 state변경으로 인해 props가 변경이 일어나지 않는 한 리렌더링 되지 않음
=> 컴포넌트 memoization
- 외부 컴포넌트 사용 시 : 각 컴포넌트 내보낼 때, 컴포넌트 이름을 React.memo로 감싸주기
export default React.memo(Box3)
- 내부 컴포넌트 사용 시 : memo()로 컴포넌트 감싸주어 사용하기
const Box2 = memo(() => {~~});
// 내부 컴포넌트 사용 예제
import React, { useState, memo } from "react";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
// memo로 감싸주어 메모이제이션 해 줌
const Box1 = memo(() => {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
console.log("Box1이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
});
const Box2 = () => {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
};
console.log("Box2이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
};
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 />
</div>
</>
);
}
export default App;
- memo로 감싸주지 않은 box2만 App 컴포넌트와 함께 리렌더링 되는 모습을 볼 수 있음
2-8 함수 최적화 - useCallback
1. useCallback이란?
- 인자로 들어오는 함수 자체를 메모이제이션 하는 hook
2. 예제와 useCallback의 필요성
- box1으로 count를 초기화 해주는 함수를 props로 내려 주어 box1 button으로 실행시켰을 때
- 위에서 메모이제이션 해주었던 box1이 리렌더링 되는 것을 확인할 수 있음
왜 리렌더링 될까?
- 함수형 컴포넌트를 사용하기 때문에, App.jsx가 리렌더링 되면서 initCount 함수가 다시 만들어지기 때문
- 함수도 객체의 한 종류이기 때문에, 다시 생성되면 그 주소값이 달라지고, 이에 따라 하위 컴포넌트의 Box1에도 새로운 주솟값을 가진 함수를 내려주기 때문에 props가 변경되었다고 인식하게 됨 -> 즉 하위 컴포넌트도 리렌더링 됨
- 따라서 useCallback을 통해 함수 자체를 메모이제이션 해주어야 함
import React, { useState, memo } from "react";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
const Box1 = memo(({ initCount }) => {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
};
console.log("Box1이 렌더링되었습니다.");
return (
<div style={boxStyle}>
<button onClick={initCount}>초기화</button>
</div>
);
});
const Box2 = () => {
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
console.log("Box2이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
};
function App() {
console.log("App 컴포넌트가 렌더링되었습니다!");
const [count, setCount] = useState(0);
const onPlusButtonClickHandler = () => {
setCount(count + 1);
};
const onMinusButtonClickHandler = () => {
setCount(count - 1);
};
// 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 />
</div>
</>
);
}
export default App;
useCallback 적용 예제
- 변경되는 함수를 메모리 공간에 저장해두고, 특정 조건이 아닐 경우에 변경되지 않도록 해주기
- useEffect 처럼 의존성 배열 필요
- 특정 state가 변경될 때, 처음 지정했던 callback 함수가 갱신되어야 하면 해당 의존성 배열에 state를 넣어주어야 함
- 빈 배열이면? 어떤 값이 바뀌어도 useCallback으로 처음 받아온 함수가 저장된 메모리 그대로 사용하고, props로 전달하기 때문에 리렌더링 발생하지 않음
// count 초기화 함수
const initCount = useCallback(() => {
setCount(0);
}, []);
Plus 예제
- count를 초기화 할 때, 어디서 초기화 되었는지 console.log로 출력하는 코드 추가
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, []);
- 하지만 실행하면 결과는 0에서 0으로 변경되었다고 나옴 -> 0일 때의 시점을 기준으로 메모리에 함수를 저장했기 때문
- 따라서 해당 콜백 함수가 실행될 때 바뀌길 원하는 count를 의존성 배열에 추가해주면, count가 변경될 때 마다 새롭게 함수 할당
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]);
2-9 값 최적화 - useMemo
1. useMemo란?
const value = useMemo(()=>{ return 반환할_함수() }, [의존성 배열]);
- 함수가 리턴하는 값이나 값 자체를 메모이제이션 하는 방법
- 동일한 값을 반환하는 함수를 계속 호출해야 할 때, 필요없는 렌더링을 방지하기 위해 처음 반환된 값을 저장하고 필요할 때 꺼내쓰는 기법
- 이미 저장된 값을 꺼내와서 쓴다 -> 캐싱한다
- 의존성 배열 값이 변경될 때만 반환할 함수가 호출됨 -> 그 이외에는 저장해 놨던 값을 가져오기만 함
2. 예제 - 오랜 시간이 걸리는 함수인 경우
- value에 useMemo로 감싸준 함수를 저장하면, 렌더링 될 때 처음 저장된 값을 가져오므로 빠르게 가져올 수 있음
import React, { useState, useMemo } from "react";
const HeavyComponent = () => {
const [count, setCount] = useState(0);
const heavyWork = () => {
for (let i = 0; i < 100; i++) {}
return 100;
};
const value = useMemo(() => heavyWork(), []);
console.log(value);
return (
<>
<p>나는 완전 무거운 컴포넌트</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
누르면 아래 count 올라감
</button>
<br />
{count}
</>
);
};
function App() {
return (
<>
<nav>네비바</nav>
<HeavyComponent />
<footer>푸터임</footer>
</>
);
}
export default App;
3. 예제 - 생존 여부가 바뀔 때만 함수를 호출하고 싶어
- 객체는 객체 내용이 같아도 주소값이 다르기 때문에, useEffect로 me 객체가 바뀔 때만 실행되도록 하여도 isAlive의 state가 변경되기 때문에 컴포넌트가 리렌더링 되면서 객체의 주소값도 계속 바뀌게 됨 -> 즉 useEffect 에서도 me가 계속 바뀐 것으로 인식됨
- 따라서 uselessCount를 바꿀때, useEffect도 계속 함께 실행되는 것
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;
- 이를 해결하기 위해 초기의 me 객체를 useMemo로 저장해주고, 의존성 배열에 isAlive를 넣어줌
- me 객체가 바뀔 때 useEffect가 실행되도록 해주었지만 이 me 객체는 useMemo로 감싸주고, 의존성 배열에 isAlive를 넣어주었기 때문에 최초의 주소값이 저장되었고, me 객체의 isAlive값이 바뀔 때만 useEffect가 실행되도록 해 줌
const me = useMemo(() => {
return {
name: "Ted Chang",
age: 21,
isAlive: isAlive ? "생존" : "사망",
};
}, [isAlive]);
useEffect(() => {
console.log("생존여부가 바뀔 때만 호출해주세요!");
}, [me]);
4. 주의할 점
- useMemo를 남발하면 별도의 메모리 확보를 너무 많이 하게 되어 성능이 악화될 수 있음
'강의 > 스파르타코딩클럽' 카테고리의 다른 글
[리액트 숙련주차] 2-11 DOM과 Virtual DOM (0) | 2023.06.30 |
---|---|
[리액트 숙련주차] 2-10 Lifecycle (1) | 2023.06.29 |
[리액트 숙련주차] 2-1~2 Styled Components (0) | 2023.06.29 |
[리액트 입문주차] 1-17 Styling ~ 1-21 컴포넌트 분리하기 (0) | 2023.06.24 |
[리액트 입문주차] 1-15~16 컴포넌트와 렌더링 (0) | 2023.06.24 |