Hook2_React 내장 Hook

Jun 23rd 2020 by jyoon

모든 코드는 git repository에서 확인 가능합니다.

2. 리액트 내장 훅 살펴보기

2.1 Consumer 컴포넌트 없이 콘텍스트 사용하기: useContext

  • 코드 5-19 훅을 사용하지 않고 콘텍스트 API를 사용하기

      import React from 'react';
      const UserContetxt = React.createContext();
      const user = { name: 'jyoon', age: 23 };
    
      //POINT1
      function ParentComponent() {
        return (
          <UserContetxt.Provider value={user}>
            <ChildComponent />
          </UserContetxt.Provider>
        );
      }
    
      //POINT2
      function ChildComponent() {
        //POINT3
        return (
          <div>
            <UserContetxt.Consumer>
              {(user) => (
                <div>
                  <div>
                    <h1> 예제 </h1>
                    <p>{`name is ${user.name}`}</p>
                    <p>{`age is ${user.age}`}</p>
                  </div>
                </div>
              )}
            </UserContetxt.Consumer>
          </div>
        );
      }
    
      export default ParentComponent;
    • ⭐️ "useContext" 훅을 이용하면 "Consumer 컴포넌트"를 사용하지 않고도 부모 컴포넌트로부터 전달된 코텍스트 데이터를 사용할 수 있다.
    • POINT3. ⭐️ 기존에는 POINT3 영역에서 콘텍스트 데이터를 사용하기 위해 콘텍스트 데이터를 전달해 주는 고차 컴포넌트를 사용
    • 코드 5-20에서 다룰 것!

      • ⭐️ 'useContext' 훅을 사용해 POINT3영역에서도 콘텍스트 데이터에 접근 할 수 있다.
  • 코드 5-20 useContext 훅 사용하기

      import React, { useContext } from 'react';
      const UserContetxt = React.createContext();
      const user = { name: 'jyoon', age: 23 };
    
      //POINT1
      function ParentComponent() {
        return (
          <UserContetxt.Provider value={user}>
            <ChildComponent />
          </UserContetxt.Provider>
        );
      }
    
      //POINT2
      function ChildComponent() {
        //POINT3
        const user = useContext(UserContetxt);
        console.log('### useContext를 통해서 return 영역이 아닌 function영역에서 conext에 접근가능!');
        return (
          <div>
            <UserContetxt.Consumer>
              {(user) => (
                <div>
                  <div>
                    <h1> 설명 </h1>
                    <p> 코드 5-20 useContext 훅 사용하기</p>
                    <p>
                      * ⭐️ useContext를 사용하면 코드5-19 POINT3 영역(useContext를 통해서 return 영역이 아닌 function영역에서
                      conext에 접근가능!)도 콘텍스트 데이터에 접근할 수 있다.
                    </p>
                  </div>
                  <hr />
                  <div>
                    <h1> 예제 </h1>
                    <p>{`name is ${user.name}`}</p>
                    <p>{`age is ${user.age}`}</p>
                  </div>
                </div>
              )}
            </UserContetxt.Consumer>
          </div>
        );
      }
      export default ParentComponent;
    • ⭐️ useContext를 사용하면 코드5-19 POINT3 영역(useContext를 통해서 return 영역이 아닌 function영역에서 conext에 접근가능!)도 콘텍스트 데이터에 접근할 수 있다.

2.2 함수형 컴포넌트에서 돔 요소 접근하기: useRef

  • 코드 5-21 훅 사용하기

      import React, { useRef } from 'react';
      export default function() {
        const inputEl = useRef(null);
        // const inputEl = useRef("최기화");
        const onClick = () => {
          console.log('### inputEl value: ', inputEl);
          if (inputEl.current) {
            inputEl.current.focus();
          }
        };
        return (
          <div>
            <div>
              <h1> 설명 </h1>
              <p> * 돔요소 접근 방법 </p>
              <p> - 클래스형 컴포넌트: createRef 함수 사용</p>
              <p> - ⭐️ 함수형 코포넌트: useRef 훅 사용</p>
            </div>
            <hr />
            <div>
              <h1> 예제 </h1>
              <div> 버튼 클릭하면 input box에 focus가 갑니다.</div>
              <input ref={inputEl} type='text' />
              <button onClick={onClick}>Focus the text</button>
            </div>
          </div>
        );
      }
    • 돔요소 접근 방법

      • 클래스형 컴포넌트: createRef 함수 사용
      • 함수형 코포넌트: useRef 훅 사용
  • 함수형 컴포넌트에서 렌더링과 무관한 값 저장하기
  • 코드 5-22 useRef 훅을 이용해서 이전 상탯값 저장하기

      import React, { useRef, useState, useEffect } from "react";
      export default function () {
        const [age, setAge] = useState(30);
        //POINT1
        const prevAgeRef = useRef(30);
        //POINT2
        useEffect(() => {
          prevAgeRef.current = age;
        }, [age]);
        //POINT3
        const prevAge = prevAgeRef.current;
        //POINT3
        const text = age === prevAge ? "same" : age > prevAge ? "older" : "yournger";
    
        return (
          <div>
              <h1> 예제 </h1>
              <p>{`age ${age} is ${text} than age ${prevAge}`}</p>
              <button
                onClick={() => {
                  const age = Math.floor(Math.random() * 50 + 1);
                  setAge(age); //POINT4
                }}
              >
                나이변경
              </button>
            </div>
          </div>
        );
      }
    • POINT1

      • 이전 상태값 저장하기 위한 용도로 useRef 사용
    • POINT2

      • age값 변경되면 prevAgeRef에 저장.
    • POINT3

      • age의 이전 상탯값을 이용
    • POINT4

      • age가 변경돼서 다시 렌더링 할때 POINT3의 prevAge는 age의 최신 상탯값으로 변경된다.
      • ⭐️ button클릭으로 age 변경 -> POINT3 코드 수행 -> "render완료 되면" useEffect(POINT2수행)으로 useRef로 설정한 prevAgeRef 업데이트
    • 이 외에도 setTimeout, setInterval 등의 함수가 반환하는 ID 값 처럼 클래스의 멤버 변수로 저장하던 값을 useRef 훅으로 저장가능

2.3 메모이제이션 훅: useMemo, useCallback

  1. useMemo
  2. 코드 5-23 useMemo 훅의 사용 예

      import React, { useMemo, useState } from 'react';
      export default function() {
        const [ value1, setValue1 ] = useState('');
        const value = useMemo(() => value1, value1);
        console.log('### useMemo return Value: ', value);
    
        const onChangeInput = (e) => {
          setValue1(e.target.value);
          console.log(value1);
        };
    
        return (
          <div>
            <div>
              <h1> 예제 </h1>
              <input type='input' value={value1} onChange={onChangeInput} />
              {/* <p>{`value is ${value}`}</p> */}
            </div>
          </div>
        );
      }
    • 계산량이 많은 함수의 반환값을 재활용하는 용도로 사용
    • useMemo 훅은 로다시 같은 라이브러리에서 제공해주는 메모이제이션과 비슷하다
    • useMemo 훅의 첫번째 매개변수로 함수를 입력
    • useMemo 훅은 이 함수가 반환한 값을 기억
    • useMemo 훅의 두번째 매개변수로 입력된 배열의 값이 변경되지 않으면 이전에 반환된 값을 재사용
    • 만약 배열의 값이 변경됐으면 첫번째 배개 변수로 입력된 함수를 실행하고 그 반환값을 기억.
  3. useCallback
  4. 코드 5-24 useCallback 훅이 필요한 예

      import React, { useState } from 'react';
      function UserEdit({ setName, setAge }) {
        setTimeout(() => {
          setName('chageName');
          setAge(100);
        }, 2000);
        return <div>THIS IS USEREDIT COMPONENT</div>;
      }
    
      function saveToServer({ name, age }) {
        console.log('### saveToServer function: ', { name, age });
      }
    
      export default function() {
        const [ name, setName ] = useState('name');
        const [ age, setAge ] = useState(10);
    
        return (
          <div>
            <hr />
            <div>
              <h1> 예제 </h1>
              <p>{`name is ${name}`}</p>
              <p>{`age is ${age}`}</p>
              {/* POINT1 */}
              <UserEdit onSave={() => saveToServer(name, age)} setName={setName} setAge={setAge} />
            </div>
          </div>
        );
      }
    • useCallback은 리액트의 렌더링 성능을 위해 제공되는 훅

      • ⭐️ 컴포넌트가 렌더링될 때 마다 함수를 생성해서 자식 컴포넌트의 속성값으로 입력하는 경우가 많다.
      • 리액트 팀에서는 최근의 브라우저에서 함수 생성이 성능에 미치는 영향은 작다고 주장
      • ⭐️ 그 보다는 속성값이 매번 변경되기 때문에 자식 컴포넌트에서 PureComponent나 React.memo를 사용해도 불필요한 렌더링이 발생한다는 문제점이 있다.
      • ⭐️ 리액트 에서는 이 문제를 해결하기 위해 useCallback 훅을 제공
  5. 코드 5-25 useCallback 훅 사용하기

      import React, { useState, useCallback } from 'react';
      function UserEdit({ setName, setAge }) {
        setTimeout(() => {
          setName('chageName');
          setAge(100);
        }, 2000);
        return <div>THIS IS USEREDIT COMPONENT</div>;
      }
      function saveToServer({ name, age }) {
        console.log('### saveToServer function: ', { name, age });
      }
      export default function() {
        const [ name, setName ] = useState('name');
        const [ age, setAge ] = useState(10);
        //POINT1
        const onSave = useCallback(() => saveToServer(name, age), [ name, age ]);
    
        return (
          <div>
            <div>
              <h1> 예제 </h1>
              <p>{`name is ${name}`}</p>
              <p>{`age is ${age}`}</p>
              <UserEdit
                // onSave={() => saveToServer(name, age)}
                onSave={onSave}
                setName={setName}
                setAge={setAge}
              />
            </div>
          </div>
        );
      }
    • 5-24에서 onSave 속성값으로 전달했던 것과 같은 함수를 useCallback 훅의 첫번째 매개변수로 입력한다.
    • ⭐️ 두번째 매개변수로 전달한 배열의 값이 변경되지 않으면 이전에 생성한 함수가 재사용된다.
    • ⭐️ 따라서 name과 age값이 변경되지 않으면 UserEdit 컴포넌트의 onSave 속성값으로 항상 같은 함수가 전달된다.

2.4 컴포넌트의 상탯값을 리덕스처럼 관리하기: useReducer

  • 코드 5-26 useReducer 훅의 사용 예

      import React, { useReducer } from 'react';
      //POINT1
      const INITIAL_STATE = { name: 'empty', age: 100 };
      //POINT1
      function reducer(state, action) {
        switch (action.type) {
          case 'setName':
            return { ...state, name: action.name };
          case 'setAge':
            return { ...state, age: action.age };
          default:
            return state;
        }
      }
    
      export default function() {
        //POINT2
        const [ state, dispatch ] = useReducer(reducer, INITIAL_STATE);
        return (
          <div>
            <div>
              <h1> 설명 </h1>
            </div>
            <hr />
            <div>
              <h1> 예제 </h1>
              <p>{`name is ${state.name}`}</p>
              <p>{`age is ${state.age}`}</p>
              <input
                type='text'
                value={state.name}
                onChange={(e) => {
                  //POINT3
                  dispatch({ type: 'setName', name: e.currentTarget.value });
                }}
              />
              <input
                type='number'
                value={state.age}
                onChange={(e) => {
                  dispatch({ type: 'setAge', age: e.currentTarget.value });
                }}
              />
            </div>
          </div>
        );
      }
    • POINT1

      • 리덕스의 리듀서와 같은 방식으로 작성한 리듀서 함수
    • POINT2

      • useReducer 훅의 매개변수로 앞에서 작성한 리듀서와 초기 상탯값을 입력
      • useReducer 훅은 상탯값과 dispatch 함수를 차례대로 반환
    • POINT3

      • 리덕스의 dispatch 함수와 같은 방식으로 사용
  • 트리의 깊은 곳으로 이벤트 처리 함수 전달하기
  • 코드 5-27 useReducer 훅과 콘텍스트 API를 이용해서 이벤트 처리 함수를 전달하기

      import React, { useReducer } from 'react';
    
      const INITIAL_STATE = { name: 'empty', age: 100 };
      function reducer(state, action) {
        switch (action.type) {
          case 'setName':
            return { ...state, name: action.name };
          case 'setAge':
            return { ...state, age: action.age };
          default:
            return state;
        }
      }
    
      const ProfileDispatch = React.createContext(null);
      const ProfileState = React.createContext(null);
    
      function Profile() {
        const [ state, dispatch ] = useReducer(reducer, INITIAL_STATE);
        return (
          <div>
            <h1> 예제 </h1>
            <p>{`name is ${state.name}`}</p>
            <p>{`age is ${state.age}`}</p>
            <ProfileState.Provider value={state}>
              <ProfileDispatch.Provider value={dispatch}>
                <SomeComponent profileInfo={state} />
              </ProfileDispatch.Provider>
            </ProfileState.Provider>
          </div>
        );
      }
    
      function SomeComponent({ profileInfo }) {
        console.log('### someComponent: ', profileInfo);
        return (
          <div>
            "SOME COMPONENT"
            <Greeting profileInfo={profileInfo} />
          </div>
        );
      }
    
      function Greeting({ profileInfo }) {
        console.log('### Greeting component: ', profileInfo);
    
        return (
          <ProfileState.Consumer>
            {(state) => (
              <ProfileDispatch.Consumer>
                {(dispatch) => (
                  <div>
                    <p>"GREETING COMPONENT"</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>
                )}
              </ProfileDispatch.Consumer>
            )}
          </ProfileState.Consumer>
        );
      }
    
      export default Profile;
    • 컴포넌트 구조

      Profile  
      ㄴ SomeComponent  
        ㄴ Greeting
    • Profile에서 reducer dispatch함수를 createContext를 통해서 바로 'Greeting' component로 전달
      (props를 통해서 SomeComponent 보내지 않고 )
    • contextAPI를 사용해서 Profile 컴포넌트에서 SomeComponent를 건너뛰고 Greeting 컴포넌트로 데이터 바로 전달

      • 사용한 contextAPI ProfileDispatch, ProfileState

2.5 부모 컴포넌트에서 접근 가능한 함수 구현하기: useImperativeHandle

  1. useImperativeHandle 훅으로 외부로 공개할 함수 정의하기
  2. 코드 5-28 부모 컴포넌트에서 접근 가능한 함수를 구현하기

      import React, { forwardRef, useState, useImperativeHandle } from "react";
    
      function Profile(props, ref) {
        const [name, setName] = useState("");
        const [age, setAge] = useState(0);
    
        useImperativeHandle(ref, () => ({
          addAge: (value) => setAge(age + value),
          getNameLength: () => name.length,
        }));
    
        return (
          <div>
            <h1> Child Component </h1>
            <div>
              <h2> 예제 </h2>
              <p>{`name is ${name}`}</p>
              <p>{`age is ${age}`}</p>
            </div>
          </div>
        );
      }
    
      export default forwardRef(Profile);
    • 코드 5-29 부모 컴포넌트에서 호출될 자식 컴포넌트
    • 부모 컴포넌트에서 접근 가능한 함수를 구현 방법

      • useImperativeHandle 훅에 두개 매개변수 지정
      • 첫번째 매개변수
        : type - ref 객체
        : useImperativeHandle 선언한 컴포넌트를 사용할 컴포넌트에서 useRef 훅으로 설정(5-29 참고)한 ref
      • 두번째 매개변수
        : type - function
        : seImperativeHandle 선언한 컴포넌트를 사용할 컴포넌트에서 호출할 함수
  3. useImperativeHandle 훅으로 정의한 함수를 외부에서 호출하기
  4. 코드 5-29 부모 컴포넌트에서 자식 컴포넌트 함수 호출하기

      import React, { useRef } from 'react';
      import Profile from './5-28';
      function Parent() {
        const profileRef = useRef();
        const onClick = () => {
          if (profileRef.current) {
            console.log('### useRef객체: ', profileRef);
            console.log('### useRef객체 current 속성 name length: ', profileRef.current.getNameLength());
            profileRef.current.addAge(5);
          }
        };
        return (
          <div>
            <Profile ref={profileRef} />
            <hr />
            <hr />
            <h1> Parent Component </h1>
            <div>
              <h2> 설명 </h2>
              <p> # 코드 5-29 부모 컴포넌트에서 자식 컴포넌트 함수 호출하기</p>
              <p> * useRef함수 return value</p>
              <p> - - useImperativeHandle 함수 두번째 변수 함수의 return value(함수가 등록된 dictionary object)</p>
              <p>* add Age 클릭시 ChildComponent의 age가 5씩 증가 합니다.</p>
            </div>
            <hr />
            <div>
              <h2> 예제 </h2>
              <button onClick={onClick}> add Age </button>
            </div>
          </div>
        );
      }
      export default Parent;
    • ⭐️ Profile 컴포넌트의 속성 값으로 ref 객체를 전달한다.
    • useRef함수 return value

      • useImperativeHandle 함수 두번째 변수 함수의 return value(함수가 등록된 dictionary object)
    • add Age 클릭시 ChildComponent의 age가 5씩 증가 합니다.

2.6 기타 리액트 내장 훅: useLayoutEffect, useDebugValue

  1. useLayoutEffect
  2. useDebugValue
  3. 코드 5-30 useDebugValue 훅을 사용하는 코드

      import { useState, useEffect, useDebugValue } from 'react';
    
      function FriendStatus(props) {
        const [ isOnline, setIsOnline ] = useState(true);
    
        // Show a label in DevTools next to this Hook
        // e.g. "FriendStatus: Online"
        useDebugValue(isOnline ? 'Online' : 'Offline');
    
        useEffect(() => {
          function handleStatusChange(status) {
            setIsOnline(status.isOnline);
          }
        });
    
        if (isOnline === null) {
          return 'Loading...';
        }
        return isOnline ? 'Online' : 'Offline';
      }
      
    • 개발 편의를 위해 제공되는 훅
    • 커스텀 훅의 내부 상태를 관찰 할 수 있따.
    • 다음 예제는 useToggle 커스텀 훅에서 디버깅을 위해 sueDebuggValue훅을 사용하는 코드
    • 리액트 개발자 도구에서 확인 할 수 있다.