[엘리스 sw 엔지니어 트랙] 50일차 Hook

오경찬·2022년 6월 19일
0

수업 50일차

어느덧 수업이 벌써 50일차가 되었다... 무려 10주!! 엘리스 코딩캠프를 시작하기 전에는 js도 모르는 상태였는데 벌써 React를 배우고 있다고 생각하니 기분이 이상했다.

이론

  • Hook: 컴포넌트에서 데이터를 관리하고 데이터가 변경될때 상호작용을 하기 위해 사용
    State Hook: 컴포넌트 내 동적인 데이터를 관리할 수 있는 Hook
    Effect Hook: 함수 컴포넌트에서 side effrct를 수행할 수 있다.
    useMemo: 계산된 값을 메모이제이션하여 재렌더링 시 불필요한 연산을 줄입
    useCallback: 함수를 메모이제이션하기 위해 사용
    useRef: 컴포넌트 생애 주기 내에서 유지할 ref객채를 반환한다.

Hook

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. Hook은 class 안에서는 동작하지 않습니다. 대신 class 없이 React를 사용할 수 있게 해주는 것

useState

v16.8 이후부터 useState 함수를 이용하여 함수형 컴포넌트에서도 state를 이용할 수 있다.
useState 함수를 호출하면 배열이 반환되는데, 첫번째 원소는 현재 상태값이고, 두번째 원소는 상태를 바꾸어 주는 setter 함수이다.
useState 함수를 호출할 때 초기 상태값을 인자로 전달한다.
setState을 호출하면 인자로 전달받은 값으로 상태값이 업데이트되고 컴포넌트가 리렌더링된다.

import React, { useState } from 'react';

function Counter() {
    // 배열 비구조화 할당
    const [number, setNumber] = useState(0);
    
    const onIncrease = () => {
        setNumber(number + 1);
    };
    
    const onDecrease = () => {
        setNumber(number - 1);
    };
    
    return (
        <div>
            <h1>{number}</h1>
            <button onClick = {onIncrease}>+1</button>
            <button onClick = {onDecrease}>-1</button>
        </div>
    );

}
export default Counter;

함수형 업데이트

만약 이전의 상태값을 참조하고 싶다면, setState에 함수 인자를 전달하면 된다.
이 함수 인자는 이전의 상태값을 인자로 받고 업데이트된 상태값을 반환한다.

// 함수형 업데이트
const onIncrease = () => {
    setNumber(prevNumber => prevNumber + 1);
}
const onDecrease = () => {
    setNumber(prevNumber => prevNumber - 1);
}

state 사용 시 주의사항

state 값을 바꾸어야 할 때는 setState 혹은 useState를 통해 전달받은 setter 함수를 사용해야만 한다.

// 잘못된 코드
this.state.number = this.state.number + 1;
this.state.array = this.state.array.push(2);
this.state.object.value = 5;

const [object, setObject] = useState({a: 1, b: 1});
object.b = 2;

배열이나 객체를 업데이트해야 할 때는 배열이나 객체 사본을 만들고 그 사본에 값을 업데이트한 후 그 사본의 상태를 setState 혹은 setter 함수통해 업데이트한다.

useEffect

useEffect를 통해 컴포넌트가 Mount (초기 렌더링), Unmount (DOM에서 제거), Update되었을 때의 특정 작업을 설정할 수 있다.

인자

useEffect는 두 개의 인자를 갖는다.
첫번째 인자: 함수 -> 함수(cleanup 함수)를 리턴할 수 있음.
두번째 인자: 의존성 배열(dependencies (deps))
두 번째 인자를 전달받았다면, deps 배열 안에 있는 값들이 변할 때만 첫 번째 인자로 전달받은 함수가 실행된다. (초기 렌더링이 완료된 후에도 실행됨.)

마운트된 이후에만 실행하고 싶을 때

useEffect에서 설정한 함수를 처음 렌더링된 이후에만 실행하고 싶을 때 두 번째 인자(deps)로 빈 배열([])을 넣어주면 된다.
-> componentDidMount처럼 실행

특정 값이 업데이트된 이후에 실행하고 싶을 때

그 특정 값을 두 번째 인자(deps)로 배열 형태로 넣어주면 된다.
-> componentDidUpdate처럼 실행 (초기 렌더링될 때도 실행됨.)

cleanup 함수

useEffect에서 함수를 리턴할 수 있는데 반환된 함수를 cleanup 함수라고 한다.
컴포넌트가 Unmout되기 직전이나 Update되기 직전에 수행할 작업을 설정할 수 있다.
deps 인자에 빈 배열을 전달하면 cleanup 함수는 컴포넌트가 Unmount되기 직전에 실행된다.
deps 배열에 값이 있다면 배열 안에 있는 값이 업데이트되기 직전에도 호출된다.

useReducer

useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 업데이트해주고 싶을 때 사용한다.
useReducer를 사용했을 때의 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다.

reducer

현재 상태(state)와 업데이트를 위해 필요한 정보를 담은 action 값을 전달받아 새로운 상태를 반환하는 함수이다.
불변성을 지키면서 업데이트된 새로운 상태를 반환해야 한다.
action 은 어떤 값도 사용 가능하다.

function reducer(state, action){
    return {};
}

state, dispatch

useReducer의 첫번째 인자에는 reducer함수를 넣고, 두번째 인자에는 초기 상태값을 넣는다.
useReducer를 호출하고 비구조화 할당을 통해 state와 dispatch를 추출한다.
state는 현재 상태값이고, dispatch는 action값을 인자로 받아 reducer함수를 호출하는 함수이다. (dispatch(action))

// Counter
import { useReducer } from 'react';

function reducer(state, action){
    switch(action.type){
        case "INCREMENT":
            return { value: state.value + 1};
        case "DECREMENT":
            return { value: state.value - 1};
        default:
            return state;
    }
}


const Counter = () => {
    const [state, dispatch] = useReducer(reducer, { value: 0 });
    return (
       <div>
           <h1>number : {state.value}</h1>
           <button onClick={() => dispatch({type: "INCREMENT"})}>+1</button>
           <button onClick={() => dispatch({type: "DECREMENT"})}>-1</button>
       </div>
    );

}

useMemo

useMemo를 이용하면 함수 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있다.
useMemo의 첫번째 인자로 어떻게 연산할지 정의하는 함수를 넣고, 두번째 인자로 deps 배열을 넣어준다.
deps 배열 안에 넣은 내용이 바뀌면, 첫번째 인자의 함수를 호출하여 연산을 진행하고, 만약 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.

const getAverage(numbers){
    console.log("평균값 계산중...");
    if (numbers.length === 0)  return 0;
    const sum = numbers.reduce((value, number) => value + number);
    return sum / numbers.length;
};


const Average = () => {
    const [list, setList] = useState([]);
    const [number, setNumber] = useState("");
    
    const onChange = (e) => {
        setNumber(e.target.value);
    };
    
    const onInsert = () => {
        setList(list.concat(parseInt(number));
        setNumber("");
    }
    
    return (
        <div>
            <input
                type="text"
                name="number"
                value={number}
                onChange={onChange}/>
            <button onClick={onInsert}>등록</button>
            <ul>
                {list.map((value, index) => (<li key={index}>{value}</li>))}
            </ul>
            <div>평균값: {getAverage(list)}</div>
        </div>
    );

}

문제점: 숫자를 등록했을 뿐만 아니라 인풋 내용이 수정될 때도 평균값을 계산하는 함수가 호출된다. 왜냐하면 인풋 내용이 수정되었을 때 리렌더링되므로 함수가 다시 호출되는 것이다. 하지만 인풋 내용이 바뀔 때에는 평균값을 계산할 필요가 없다.
useMemo를 이용하면 이러한 작업을 최적화할 수 있다. 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았을 때는 이전에 연산했던 결과를 재사용한다.

import { useMemo } from "react";
const avg = useMemo(() => getAverage(list), [list]);
// list 배열의 내용이 바뀔 때만 함수 호출

useCallback

useMemo와 비슷하게 주로 렌더링 성능을 최적화해야 할 때 사용한다.
useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용한다.
첫번째 인자로 생성하고 싶은 함수를 넣고, 두번째 인자로 deps 배열을 넣어준다.
deps 배열 안에 있는 내용이 바뀌면, 첫번째 인자의 함수를 새로 생성하고, 아니면 이전에 렌더링될 때 생성한 함수를 재사용한다.
만약 deps에 빈 배열을 넣게 되면, 컴포넌트가 처음 렌더링될 때 만들었던 함수를 계속 재사용한다.
반면, deps 인자에 아무것도 넣지 않는다면 컴포넌트가 매번 렌더링될 때마다 첫번째 인자의 함수를 생성한다.
함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 인자에 포함해야 한다.

import { useState, useMemo, useCallback } from "react";

const getAverage(numbers){
    console.log("평균값 계산중...");
    if (numbers.length === 0)  return 0;
    const sum = numbers.reduce((value, number) => value + number);
    return sum / numbers.length;
};


const Average = () => {
    const [list, setList] = useState([]);
    const [number, setNumber] = useState("");
    
    const onChange = useCallback((e) => {
        setNumber(e.target.value);
    }, []);
    
    const onInsert = useCallback(() => {
        setList(list.concat(parseInt(number));
        setNumber("");
    }, [list, number]);
    
    const avg = useMemo(() => getAverage(list), [list]);
    
    return (
        <div>
            <input
                type="text"
                name="number"
                value={number}
                onChange={onChange}/>
            <button onClick={onInsert}>등록</button>
            <ul>
                {list.map((value, index) => (<li key={index}>{value}</li>))}
            </ul>
            <div>평균값: {avg}</div>
        </div>
    );

}

useRef

특정 DOM 선택

리액트에서 특정 DOM을 선택할 때 useRef를 사용한다.
DOM에 직접적으로 접근해야 하는 경우
특정 input에 포커스 주기
스크롤 박스 조작하기
canvas 요소에 그림 그리기 등
useRef() 를 사용하여 Ref 객체를 만들고, 이 객체를 우리가 선택하고 싶은 DOM의 ref 값으로 설정한다.
Ref 객체의 .current 값은 앞서 선택한 DOM 을 가르키게 된다.

import React, { useState, useRef } from 'react';

function InputSample() {
  const [inputs, setInputs] = useState({
    name: '',
    nickname: ''
  });
  
  const nameInput = useRef();

  const { name, nickname } = inputs;

  const onChange = e => {
    const { value, name } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };

  const onReset = () => {
    setInputs({
      name: '',
      nickname: ''
    });
    nameInput.current.focus();
  };

  return (
    <div>
      <input
        name="name"
        placeholder="이름"
        onChange={onChange}
        value={name}
        ref={nameInput}
      />
      <input
        name="nickname"
        placeholder="닉네임"
        onChange={onChange}
        value={nickname}
      />
      <button onClick={onReset}>초기화</button>
      <div>
        <b>: </b>
        {name} ({nickname})
      </div>
    </div>
  );
}

export default InputSample;

컴포넌트 안의 변수

useRef를 이용하여 컴포넌트 안에서 조회 및 수정할 수 있는 변수를 관리할 수 있다.
이때 useRef로 관리하는 변수가 바뀐다고 해서 컴포넌트가 리렌더링되지 않는다.
useRef 로 관리하고 있는 변수는 설정 후 바로 조회할 수 있다.
다음과 같은 값 관리

  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치

일반 로컬 변수 대신 useRef를 이용하는 이유?

함수형 컴포넌트도 함수이기 때문에 re-rendering 되면 로컬 변수들을 초기화한다.
반면 useRef로 만들어진 객체는 React가 만든 전역 저장소에 저장되기 때문에 리렌더링되더라도 마지막으로 업데이트한 current 값이 유지된다.

클래스형 컴포넌트의 ref

콜백함수를 통한 ref 설정

<input ref = { (ref) => {this.input = ref}} />

createRef를 통한 ref 설정
import { Component } from "react";

class RefSample extends Component {
    input = React.createRef();
    
    handleFocus = () => {
        this.input.current.focus();
    }
    
    render() {
        return (
            <div>
               <input ref = {this.input}/>
            </div>
        );
    }
}
profile
코린이 입니당 :)

0개의 댓글