본문 바로가기
강의/스파르타코딩클럽

[리액트 숙련주차] 2-3~9 React Hooks

by gardenii 2023. 6. 29.

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를 남발하면 별도의 메모리 확보를 너무 많이 하게 되어 성능이 악화될 수 있음