[React.js]리액트 훅 규칙, Context API, 내장 훅

2021. 7. 13. 03:04Web_Programming/React

 

📍 훅 규칙

 

💡 훅 사용 순서는 늘 같아야 함

👉 if문 for문 등에 넣어 조건적으로 호출되면 안 됩니다.

👉 if 문 return 뒤에 사용도 마찬가지입니다.

 

💡  함수형 컴포넌트나 커스텀 훅에서만 호출 가능

 

📍 Context API

 

속성 값을 하위 컴포넌트로 넘겨 사용할 경우,

컴포넌트 트리가 깊다면 속성값을 여러 컴포넌트에 걸쳐 넘겨줘야 하는 불편함이 있습니다.

 

이를 해결할 수 있는 방법으로 context API가 있습니다.

 

import React, {useContext, createContext } from 'react';

const UserContext = createContext('unknown');
export default function App() {
  return (
    <div>
      <UserContext.Provider value="horong">
        <div>상단 메뉴</div>
          <Profile />
        <div>하단 메뉴</div>
      </UserContext.Provider>
    </div>
  );
}
function Profile() {
  return (
    <Greeting />
    {/* ... */}
  );
}
/*
function Greeting() {
  return (
    <UserContext.Consumer>
      {username => <p>{`${username}님 안녕하세요`}</p>}
    </UserContext.Consumer>
  )
}
*/
function Greeting() {
  const username=useContext(UesrContext);
  return (<p>{`${username}님 안녕하세요`}</p>)
}

💡 Provider를 이용하여 값 설정

 

💡 Consumer 를 이용하여 값 사용

 

👉 가장 가까운 상위 Provider 컴포넌트를 찾아 값을 받는다.

👉 못 찾으면 초기 설정 값으로 사용.

 

❗ Provider로 값 변경 시 Cousumer까지의 컴포넌트 리 랜더링

+ 중간 컴포넌트 리랜던링 안 해도 잘적용 

useContext훅으로 사용 가능

 

 

💡 속성 값 변경 함수

 

속성값 변경 함수 또한 Context로 넘겨줄 수 있습니다.

 

상위 컴포넌트

import React, { createContext } from 'react';


const UserContext = createContext({ username: 'unknown', helloCount: 0 });
const SetUserContext = createContext(() => {});

export default function App() {
  const [user, setUser] = useState({ username: 'horong', helloCount: 0 });

  return (
    <div>
      <SetUserContext.Provider value={setUser}>
        <UserContext.Provider value={user}>
          <div>상단 메뉴</div>
            <Profile username="horong" />
          <div>하단 메뉴</div>
        </UserContext.Provider>
      <SetUserContext.Provider>
    </div>
  );
}

 

하위 컴포넌트

import React, { useContext, createContext } from 'react';

export default function Greeting() {
  const setUser = useContext(SetUserContext);
  const { username, helloCount } = useContext(UserContext);

  return (
    <>
      <p>{`${username}님 안녕하세요`}</p>
      <p>{`인사 횟수: ${helloCount}`}</p>
      <button onClick={() => setUser({ username, helloCount: helloCount + 1})}>
        인사하기
      </button>
    </>
  );
}

 

❗ Provider는 매번 랜더링 될 때마다 새로운 객체가 생기며,

Consumer의 컴포넌트가 사용하지 않는 속성 값의 변경으로도 리 랜더링 되는 문제가 발생 

 

 

👉 해결 방법, 새로운 객체가 매번 생기지 않게 하나의 훅으로 관리

import React, { createContext } from 'react';

const UserContext = createContext({ username: 'unknown', age: 0 });

export default function App() {
  /*
  const [username, setUsername] = useState('');
  const [age, setAge] = useState(0);
  */
  const [user, setuser] = useState({ username: 'horong', age: 23 });
  
  return (
    <div>
      <UserContext.Provider value={user}>
          <Profile />
      </UserContext.Provider>
    </div>
  );
}

+ Provider 안에서 consumer를 사용해야 Provider에 도달하여 값을 받을 수 있습니다.

 

📍 useRef

 

ref속성 값을 이용하여 돔 요소에 접근

👉 이를 리액트에서는 useRef 훅으로 접근 가능하게 해 줍니다.

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

export default function App() {
  const inputRef = useRef();
  useEffect(() => {
    inputRef.current.focus();
  }, [])
  
  return (
    <div>
      <input type="text" ref={inputRef}/>
      <button>저장</button>
    </div>
  )
}

 

❗ ref는 랜더링 할 때마다 새로운 함수를 호출합니다

    👉 사용자의 입력값이 매번 초기화되는 문제

import React, { useState } from 'react';

export default function App() {
  const [text, setText] = useState(INITIAL_TEXT);
  const [showText, setShowText] = useState(true);
  
  return (
    <div>
      {showText && (
        <input type="text" 
               ref={ref => ref && setText(INITIAL_TEXT)}
               value={text}
               onChange={e => setText(e.target.value)}
        />
      )}
      <button onClick={() => setShowText(!showText)} >보이기/가리기</button>
    </div>
  )
}

const INITIAL_TEXT = '안녕하세요';

 

💡 이를 해결할 수 있는 방법으로 useCallback을 사용합니다. 

 

useCallback 훅의 메모 이제이 션 덕분에 한번 생성된 함수를 재사용할 수 있습니다.

따라서 ref 속성이 계속 새로운 함수로 인식하지 않도록 도와줍니다.

import React, { useState } from 'react';

export default function App() {
  const [text, setText] = useState(INITIAL_TEXT);
  const [showText, setShowText] = useState(true);
  
  const setInitialText = useCallback(ref => ref && setText(INITIAL_TEXT));
  
  return (
    <div>
      {showText && (
        <input type="text" 
               ref={setInitialText}
               value={text}
               onChange={e => setText(e.target.value)}
        />
      )}
      <button onClick={() => setShowText(!showText)} >보이기/가리기</button>
    </div>
  )
}

const INITIAL_TEXT = '안녕하세요';

 

💡 돔을 여러 개 사용할 때

 

useRef를 여러개 매번 사용할 때 문제가 발생할 수 있습니다.

이때는 함수를 이용하여 ref를 관리합니다.

 

여러 개의 돔들을 한 번에 관리할 수 있는 boxListRef 객체를 만들고, 오브젝트로 초기화를 합니다.

그 후, BOX_LIST 데이터를 렌더링을 해주면서 동시에 각각의 div 참조 객체를 boxListRef에 넣어서 관리합니다.

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

export default function App() {
  const boxListRef = useRef({});

  return (
    <div>
      <div
        style={{
          display: 'flex',
          flexWrap:'wrap',
          width: '100%',
          height: '100vh',
        }}
      >
        {BOX_LIST.map(item => (
          <div
            key={item.id}
            ref={ref => (boxListRef.current[item.id] = ref)}
            style={{
              flex: '0 0 auto',
              width: item:width,
              height: 100,
              backgroundColor: 'yellow',
              border: 'solid 1px red',
            }}
          >{`box+${item.id}`}</div>
        ))}
      </div>
    </div>
  );
}

const BOX_LIST = [
  { id: 1, width: 50 },
  { id: 2, width: 30 },
  { id: 3, width: 10 },
  { id: 4, width: 50 },
  { id: 5, width: 70 },
  { id: 6, width: 30 },
  { id: 7, width: 20 }
];

 

 

📍 리액트 내장 훅

 

💡 useRef

useRef는 돔의 참조 객체 외에도 렌더링과 상관없는 값을 다루는 경우에도 유용하게 사용할 수 있습니다.

import React, { userState, useRef, useEffect } from 'react';

export default function App() {
  const timerIdRef = useRef(-1);
  
  useEffect((0 => {
    timerIdRef.current = setTimeout(() => {}, 1000)
  });
  
  useEffect((0 => {
    if(timerIdRef.current >= 0) {
      clearTimeout(timerIdRef.current);
    }
  });
}

👉 렌더링과 무관한 데이터들은 useRef를 사용하여 다루어 주는 것이 좋습니다.

 

 

💡useMemo

 

useMemo는 계산량이 많은 함수의 반환 값을 재활용하는 용도로 사용됩니다.

import React, { userState, useMemo } from 'react';

export default function App() {
  const [v1, setV1] = useState(0);
  const [v2, setV2] = useState(0);
  const [v3, setV3] = useState(0);
  
  const value = useMemo(() => runExpensiveJob(v1, v2), [v1, v2]);
  
  return (
    <>
      <p>{`value is ${value}`}</p>
      <button
        onClick={() => {
          setV1(Math.random());
          setV2(Math.random());
        }}
      >
        v1/v2 수정
      </button>
      <p>{`v3 is ${v3}`}</p>
      <button onClick={() => setV3(Math.random())}>v3 수정</button>
    </>
  )
}

function runExpensiveJob(v1, v2) {
  return v1 + v2;
}

 

useMemo 안에 첫 번째 매개변수 : 복잡한 계산을 하는 함수

👉 리액트가 함수의 반환 값을 기억

useMemo 안에 두 번째 매개변수 : 이 함수는 v1, v2 값이 하나라도 변경되면 실행

 

두 번째 인자 배열 값(useEffect와 마찬가지로 의존성 배열)이 변경되지 않았다면

👉 이전에 실행된 함수의 결괏값을 재활용 

 

💡 useCallback

 

랜더링으로 새로운 함수가 만들어져서 전달되면, React.memo를 사용하여도 필요 없는 리 랜더링이 이뤄집니다.

import React, { userState } from 'react';

export default function App() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [v1, setV1] = useState(0);
  
  return (
    <div>
      <p>{`name is ${name}`}</p>
      <p>{`age is ${age}`}</p>
      <UserEdit
        onSave={() => saveToServer(name, age)}
        setName={setName}
        setAge={setAge}
      />
      <p>{`v1 is ${v1}`}</p>
      <button onClick={() => setV1(Math.random())}>v1 수정</button>
    </div>
  )
}

const UserEdit = React.memo(function ({ onSave, setName, setAge }) {
  console.log('UserEdit render');
  return null;
})

function saveToServer(name, age) {}

 

👉 useCallback 훅을 사용하여 name, age 값이 변경될 때만 새로운 함수가 생성되도록 작성을 했습니다.

+useCallback도 의존성 배열로 관리

 

import React, { userState, useCallback } from 'react';

export default function App() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [v1, setV1] = useState(0);
  
  const onSave = useCallback(() => saveToServer(name, age), [name, age]);
  
  return (
    <div>
      <p>{`name is ${name}`}</p>
      <p>{`age is ${age}`}</p>
      <UserEdit
        onSave={onSave}
        setName={setName}
        setAge={setAge}
      />
      <p>{`v1 is ${v1}`}</p>
      <button onClick={() => setV1(Math.random())}>v1 수정</button>
    </div>
  )
}

const UserEdit = React.memo(function ({ onSave, setName, setAge }) {
  console.log('UserEdit render');
  return null;
})

function saveToServer(name, age) {}

 

 

💡 useReducer

useReducer는 여러 개의 상태 값을 한 번에 사용하는 경우에 적합한 리액트 훅입니다.

 

리듀서 함수

 

상탯값 변경을 위한 액션을 수행해주는 함수

const INITIAL_STATE = { name: 'setAge', age: e.currentTarget.value };
const MAX_AGE = 50;
function reducer(state, action) {
  switch (action.type) {
    case 'setName':
      return { ...state, name: action.name };
    case 'setAge':
      if (action.age > MAX_AGE){
        return { ...state, age: MAX_AGE };
      } else {
        return { ...state, age: action.age };
      }
    default:
  }
}

 

 

👉 dispatch 함수를 호출하여 상태 값을 변경

import React, { useReducer } from 'react';

export default function App() {
  const [state, dispatch] = useReducer(reducer, INITTIAL_STATE);
  
  return (
    <div>
      <p>{`name is ${state.name}`}</p>
      <p>{`age is ${state.age}`}</p>
      <input
        type="text"
        value={state.name}
        onChange{e => dispatch({ type: 'setName', name: e.currentTarget.value })}
      />
      <input
        type="number"
        value={state.age}
        onChange{e => dispatch({ type: 'setAge', age: e.currentTarget.value })}
      />
    </div>
  );
}

 

👉 이렇게 reducer를 사용하면 상태값을 변경하는 로직을 분리할 수 있다는 것이 장점

 

 

useReducer 훅이랑 Context API를 같이 이용

 

👉 자식 컴포넌트로부터 발생한 이벤트에서 상위 컴포넌트의 상태 값을 변경해야 하는 경우

👉 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 쉽게 전달할 수 있습니다.

 

 

콘텍스트 provider에 useReducer의 상태 변환 함수인 dispatch 함수를 내려줍니다.

👉 필요한 컴포넌트에서 dispatch 함수를 이용하여 상태 값을 변경할 수 있습니다.

 

import React, { useReducer } from 'react';

export const ProfileDispatch = React.createContext(null);

export default function App() {
  const [state, dispatch] = useReducer(reducer, INITTIAL_STATE);
  
  return (
    <div>
      <p>{`name is ${state.name}`}</p>
      <p>{`age is ${state.age}`}</p>
      <ProfileDispatch.Provider value={dispatch}>
        <SomeConponent />
      </ProfileDispatch.Provider>
    </div>
  );
}

 

💡 useImperativeHandle

 

함수형 컴포넌트에도 Class처럼 멤버 변수나 멤버 함수가 있는 것처럼 사용할 수 있습니다.

 

 

  • ref 속성 값을 받아 useImperativeHandle의 첫 번째 매개변수로 입력
  • 두 번째 매개변수로 함수를 입력하고 있는데, 이 함수가 반환한 값이 이 부모의 ref 객체가 참조하는 값

 

❗ ref 속성을 받아서 사용하기 위해서 forwardRef 함수를 사용해야 합니다.

👉 부모 컴포넌트는 useRef 훅을 통해서 선언했던 ref 객체에서 2개의 함수를 자유롭게 사용할 수 있습니다.

import React, { forwardRef, useState, useImperativeHandle } from 'react';

fucntion Profile(_, ref) {
  const [name, setName] = userState('horong');
  const [age, setAge] = userState(0);
  
  useImperativeHandle(ref, () => ({
    addAge: value => setAge(age + value),
    getNameLength: () => name.length,
  }));
  
  return (
    <div>
      <p>{`name is ${name}`}</p>
      <p>{`age is ${age}`}</p>
    </div>
  )
}

export default forwardRef(Profile);

 

 

 

상위 컴포넌트

 

import React, { useRef } from 'react';
import Profile from './Profile';

export default function App() {
  const profileRef = useRef();
  const onClick = () => {
    if (profileRef.current) {
      console.log('current name length:', profileRef.current.getNameLength())
      profileRef.current.addAge(5);
    }
  }
  
  return (
    <div>
      <Profile ref={profileRef} />
      <button onClick={onClick}>add age 5</button>
    </div>
  )
}

 

 

💡 useLayoutEffect

useLayoutEffect 훅은 useEffect 훅과 거의 비슷하게 동작하지만 

부수 효과 함수가 동기로 호출된다는 차이점이 있습니다.

 

  • useEffect 함수는 실제 돔에 반영된 다음 화면이 그려진 후에 동작
  • useLayoutEffect는 실제 돔에 반영된 다음 화면이 그려지기 전에 동작

 

❗ useLayoutEffect 훅에 연산을 많이 하면 브라우저에 무리가 가므로, useEffect 훅을 사용하는 것이 성능상 이점이 있습니다.

 

 

👉 렌더링 직후에 돔 요소의 값을 읽어 들이는 경우

👉 조건에 따라서 컴포넌트를 다시 렌더링 하고 싶은 경우에 사용

 

 

 

 

 

반응형