NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    JavaScript 2부 - React의 Hooks에 대해 알아보자!

    페이지 정보

    본문

    [ 1부 - React Hooks (useState, useEffect) ]

     

    3. useContext

    React 공식 문서의 설명에는 context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이타를 제공할 수 있다고 합니다. 일반적으로 React 어플리케이션에서 데이타는 props를 통해서 부모에서 자식에게 전달되지만, 어플리케이션 안의 여러 컴포넌트들에게 props를 전달해줘야 하는 경우 context를 이용하면 명시적으로 props를 넘겨주지 않아도 값을 공유할 수 있습니다. src 폴더에 Shop.js 파일을 하나 만듭니다.

    import React from "react";
    import { AppContext } from "./App";
    
    const Shop = () => {
      return (
          <AppContext.Consumer>
            {(macro) => (
              <>
                <h3>AppContext에 존재하는 name의 값은 {macro.name}입니다.</h3>
                <h3>AppContext에 존재하는 cost의 값은 {macro.cost}입니다.</h3>
              </>
            )}
          </AppContext.Consumer>
      );
    };
    
    export default Shop;

     

    App.js 의 파일 내용을 아래와 같이 변경하세요.

    import React, { createContext } from "react";
    import Shopfrom "./Shop";
    
    export const AppContext = createContext();
    
    const App = () => {
      const macro = {
        name: "엔지엠 에디터",
        cost: "10,000"
      };
    
      return (
        <>
          <AppContext.Provider value={macro}>
            <div>
              <Shop />
            </div>
          </AppContext.Provider>
        </>
      );
    };
    
    export default App;

     

    기본적인 리액트의 콘텍스트(AppContext)를 만들었습니다. 터미널에서 npm start로 리액트 어플리케이션을 실행하세요. 아래와 같이 콘텍스트의 값이 표시됩니다.

    VuujtoL.png

     

     

    이 예제에선 하나의 컴포넌트에서만 콘텍스트를 사용했지만 코드가 늘어나면 여러 컴포넌트에서 AppContext 컨슈머를 계속 만들어야 하는데 이 작업이 귀찮고, 코드도 점점 어려워지는 단점이 있습니다. 리액트에서 Context를 만드는 방법은 CreateContext로 전역 데이타 공간을 만듭니다. 이 때 Provider와 Consumer 컴포넌트를 반환합니다. 아래 코드는 좀 더 편리하게 사용할 수 있는 useContext를 적용한 Shop 컴포넌트입니다.

    import React, { useContext } from "react";
    import { AppContext } from "./App";
    
    const Shop = () => {
      const macro = useContext(AppContext);
      return (
        <>
          <h3>AppContext에 존재하는 name의 값은 {macro.name}입니다.</h3>
          <h3>AppContext에 존재하는 cost의 값은 {macro.cost}입니다.</h3>
        </>
      );
    };
    
    export default Shop;

     

    실행 결과는 동일합니다. 다만, 어플리케이션 전반에 걸쳐서 사용해야 하는 데이타를 컴포넌트별로 모두 넘기는건 코드를 관리하는 측면에서 좋은 선택은 아닐겁니다. 필요한 것들은 AppContext에 두고, 사용하세요. View의 디자인 요소를 변경하는 방법입니다.

    import React, { createContext, useContext } from "react";
    
    const ThemeContext = createContext('orange');
    
    const Shop = () => {
      const theme = useContext(ThemeContext);
      const style = {
        width: '36px',
        height: '36px',
        background: theme
      };
      return <div style={style} />;
    };
    
    export default Shop;

     

    App.js도 아래와 같이 변경 해주세요.

    import React from "react";
    import Shop from "./Shop";
    
    const App = () => {
        return <Shop />;
    };
    
    export default App;

     

    Ctrl+S로 저장하면 웹브라우저에 오랜지색 사각형이 표시됩니다.

    zTTPHO8.png

     

     

    4. useReducer

    useState보다 컴포넌트에서 더 다양한 상황에 따라 상태를 업데이트 해주고 싶을 때 사용하는 훅입니다. 리듀서는 현재 상태와 업데이트를 위해 필요한 정보를 담는 액션(Action)을 전달 받아 새로운 상태를 반환하는 함수입니다. 리듀서 함수에서 새로운 상태를 만들때는 꼭 불변성을 지켜줘야 합니다.

    function reducer(state, action) {
      return { ... }; // 불변성을 지키면서 업데이트한 새로운 상태를 반환합니다
    }

     

    액션 값은 주로 다음과 같은 형태로 이루어져 있습니다.

    {
    type: 'INCREMENT',
    // 다른 값들이 필요하다면, 추가적으로 들어감
    }

     

    Redux에서는 어떤 액션인지 알려주는 type 필드가 꼭 있어야 하지만, useReducer는 사용하는 액션안에 type을 가지고 있을 필요가 없습니다. 그리고, 객체가 아니라 문자열이나 숫자여도 상관이 없습니다. 아래 코드는 1부에서 만들었던 카운터를 useReducer로 다시 구현해봤습니다. 이러면, 차이점을 좀 더 명확하게 알 수 있겠죠? Counter.js에 코드를 아래와 같이 변경 해줍니다.

    import React, { useReducer } from 'react';
    
    function reducer(state, action) {
      // action.type 에 따라 다른 작업 수행
      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>
          <p>
            현재 카운터 값은 <b>{state.value}</b> 입니다.
          </p>
          <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
          <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
        </div>
      );
    };
    
    export default Counter;

     

    useReducer의 첫번째 파라메터는 리듀서 함수명을 입력하고, 두번째 파라메터에는 리듀서의 기본 값을 넣어줍니다. 이 훅(Hook)을 사용했을 때 state 값과 dispatch 함수를 받아오게 됩니다. 여기서 state는 현재 상태고, dispatch는 액션을 발생시키는 함수입니다. useReducer를 사용했을 때의 가장 큰 장점은 컴포넌트가 업데이트될 때 로직을 바깥으로 빼낼 수 있다는 점입니다. App.js는 아래와 같이 변경하고, 카운터를 테스트 해보세요. 이전과 동일하게 동작하는걸 확인할 수 있습니다.

    import React from 'react';
    import Counter from './Counter';
    
    const App = () => {
      return <Counter />;
    };
    
    export default App;

     

    이번에는 useReducer를 사용하여 Info 컴포넌트의 텍스트박스 상태를 관리하는 방법에 대해 알아보겠습니다. 기존에 텍스트박스가 여러개여서 useState를 여러번 사용했습니다. useReducer를 사용하면 기존에 클래스형 컴포넌트에서 텍스트박스(Input Control) 태그에 name 값을 할당하고, e.target.name을 참조하여 setState를 해준 것과 유사한 방식으로 작업을 처리할 수 있습니다.

    import React, { useReducer } from 'react';
    
    function reducer(state, action) {
      return {
        ...state,
        [action.name]: action.value
      };
    }
    
    const Info = () => {
      const [state, dispatch] = useReducer(reducer, {
        name: '',
        nickname: ''
      });
      const { name, nickname } = state;
      const onChange = e => {
        dispatch(e.target);
      };
    
      return (
        <div>
          <div>
            <input name="name" value={name} onChange={onChange} />
            <input name="nickname" value={nickname} onChange={onChange} />
          </div>
          <div>
            <div>
              <b>이름:</b> {name}
            </div>
            <div>
              <b>닉네임: </b>
              {nickname}
            </div>
          </div>
        </div>
      );
    };
    
    export default Info;

     

    App.js도 코드를 변경 해줍니다.

    import React from 'react';
    import Info from './Info';
    
    const App = () => {
      return <Info />;
    };
    
    export default App;

     

    앱을 실행하면 1부에서 만들었던 것과 동일하게 동작하는걸 확인할 수 있습니다. useReducer의 액션은 그 어떤 값이 되어도 상관이 없습니다. 그래서 이벤트 객체가 지니고 있는 e.target 값 자체를 액션 값으로 사용했습니다. 이런식으로 콘트롤을 관리하면 아무리 인풋의 개수가 많아져도 코드를 짧고 깔끔하게 유지할 수 있습니다.

    GknI72O.png

     

     

    5. useMemo

    useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있습니다. 먼저 리스트에 숫자들을 추가하면 해당 숫자들의 평균을 나타내는 함수형 컴포넌트를 작성 해봅니다. src 폴더에 Average.js 파일을 하나 생성하고, 코드를 아래와 같이 작성합니다. 

    import React, { useState } from 'react';
    
    const getAverage = numbers => {
      console.log('평균값 계산중..');
      if (numbers.length === 0) return 0;
      const sum = numbers.reduce((a, b) => a + b);
      return sum / numbers.length;
    };
    
    const Average = () => {
      const [list, setList] = useState([]);
      const [number, setNumber] = useState('');
    
      const onChange = e => {
        setNumber(e.target.value);
      };
      const onInsert = e => {
        const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
      };
    
      return (
        <div>
          <input value={number} onChange={onChange} />
          <button onClick={onInsert}>등록</button>
          <ul>
            {list.map((value, index) => (
              <li key={index}>{value}</li>
            ))}
          </ul>
          <div>
            <b>평균 값:</b> {getAverage(list)}
          </div>
        </div>
      );
    };
    
    export default Average;

     

    App.js를 수정하고, 실행 해보세요.

    import React from 'react';
    import Average from './Average';
    
    const App = () => {
      return <Average />;
    };
    
    export default App;

     

    브라우저에서 숫자들을 등록하면 실시간으로 평균값이 계산되어 표시됩니다.

    9hvMwGB.png

     

     

    F12를 눌러서 개발자 도구를 열고, 다시 숫자들을 등록 해보세요. 숫자를 등록할 때뿐만 아니라 텍스트박스에 내용이 수정될 때도 getAverage 함수가 호출되고 있는것을 확인할 수 있습니다. 텍스트박스에 숫자를 입력할때는 평균 값을 계산할 필요가 없습니다. 등록 버튼을 클릭할 때 계산하면 되기 때문입니다. 이렇게 렌더링할 때마다 계산을 하는건 낭비입니다.

    ptTSQMh.png

     

     

    Average.js 파일의 내용에서 useMemo를 사용 해봅시다. getAverage 함수를 useMemo에 등록하고, 숫자 목록이 바뀔 때만 호출하도록 하고 있습니다.

    const avg = useMemo(() => getAverage(list), [list]);

    mq4VlZ8.png

     

     

    Average.js 의 전체 코드는 아래와 같습니다.

    import React, { useState, useMemo } from 'react';
    
    const getAverage = numbers => {
      console.log('평균값 계산중..');
      if (numbers.length === 0) return 0;
      const sum = numbers.reduce((a, b) => a + b);
      return sum / numbers.length;
    };
    
    const Average = () => {
      const [list, setList] = useState([]);
      const [number, setNumber] = useState('');
    
      const onChange = e => {
        setNumber(e.target.value);
      };
      const onInsert = e => {
        const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
      };
    
      const avg = useMemo(() => getAverage(list), [list]);
    
      return (
        <div>
          <input value={number} onChange={onChange} />
          <button onClick={onInsert}>등록</button>
          <ul>
            {list.map((value, index) => (
              <li key={index}>{value}</li>
            ))}
          </ul>
          <div>
            <b>평균 값:</b> {avg}
          </div>
        </div>
      );
    };
    
    export default Average;

     

    6. useCallback

    useCallback은 useMemo와 비슷한 동작을 하는 함수입니다. 주로 렌더링 성능을 최적화해야 하는 상황에서 사용하는데요. 이 Hook을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있습니다. 우리가 방금 구현한 Average 컴포넌트를 보면 onChange와 onInsert 함수를 선언했습니다. 이렇게 선언을 하게되면 컴포넌트가 리렌더링 될 때마다 이 함수들이 새로 생성됩니다. 대부분의 경우에는 이런 방식이 문제가 되지 않지만, 컴포넌트의 렌ㄴ더링이 자주 발생하거나 렌더링 해야 할 컴포넌트의 개수가 많아진다면 코드를 리팩토링하면서 최적화하는게 좋습니다.

    import React, { useState, useMemo, useCallback } from 'react';
    
    const getAverage = numbers => {
      console.log('평균값 계산중..');
      if (numbers.length === 0) return 0;
      const sum = numbers.reduce((a, b) => a + b);
      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(
        e => {
          const nextList = list.concat(parseInt(number));
          setList(nextList);
          setNumber('');
        },
        [number, list]
      ); // number 혹은 list 가 바뀌었을 때만 함수 생성
    
      const avg = useMemo(() => getAverage(list), [list]);
    
      return (
        <div>
          <input value={number} onChange={onChange}  />
          <button onClick={onInsert}>등록</button>
          <ul>
            {list.map((value, index) => (
              <li key={index}>{value}</li>
            ))}
          </ul>
          <div>
            <b>평균값:</b> {avg}
          </div>
        </div>
      );
    };
    
    export default Average;

     

    useCallback의 첫번째 파라메터는 생성하고자 하는 함수를 넣어주고, 두번째 파라메터는 배열을 넣어줍니다. 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해주어야 하는지 명시해야 합니다. onChange처럼 빈 배열을 넣게 되면 컴포넌트가 렌더링 될 때 단 한번만 함수가 생성되고, onInsert처럼 배열 안에 number와 list를 넣게 되면 텍스트박스의 내용이 바뀌거나 새로운 항목이 추가될 때마다 함수가 다시 생성됩니다. 함수 내부에서 기존의 상태 값을 의존해야 할 때는 꼭 두번째 파라메터 안에 포함해줘야 합니다. 아래 useCallbackuseMemo는 코드는 다르지만 동일한 동작입니다.

    useCallback(() => {
      console.log('hello world!');
    }, [])
    
    useMemo(() => {
      const fn = () => {
        console.log('hello world!');
      };
      return fn;
    }, [])

     

    useMemo는 값을 재사용하기 위한 용도고, useCallback은 함수를 재사용하기 위한 Hook입니다. 일반적으로 useCallback은 Frontend에서 Backend로 API(RESTful)를 호출할 때 사용합니다.

      const fetchDepthDataHandler = useCallback(async (request: any) => {
        try {
          const resquestOptions = {
            method: 'POST',
            headers: {
              'Content-Type': 'application.json',
            },
          };
          const params = new URLSearchParams({
            conditionDesignData: JSON.stringify(request),
          }).toString();
          const response = await fetch(
            `http://localhost:2023/api/v1/common/condition/search-condition?${params}`,
            resquestOptions
          );
    
          if (!response.ok) {
            throw new Error(response.error.text);
          }
    
          const data = await response.json();
          setSelectOptionItems((prevState: any) => {
            return { ...prevState, [request.depth]: data.result.items };
          });
        } catch (error: any) {
          throw error;
        }
      }, []);

     

    이외에도 몇가지 Hook이 더 존재합니다. CustomState도 만들 수 있습니다. 리액트를 공부하면서 기본적으로 알아야할 상태 관리 방법에 대해 알아봤는데요. 대부분의 내용은 인터넷에서 검색 해보면 더 자세하게 설명되어 있는것들이 많습니다. 리액트 공식 사이트에서 예제들을 다운로드 받아서 직접 실행해보고, 개발자도구에서 로그를 찍어보면 어떻게 동작하는지 세세하게 파악할 수 있습니다. Backend를 디자인해두고, Frontend도 기본적인 구조와 환경을 구축해야 하는데요. 지식이 없이 업무를 할당 받다보니 주말에도 공부를 하게 되는군요-_-;

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.