Hook1_Hook Basic

Jun 23rd 2020 by jyoon

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

1 리액트 훅 기초 익히기

  • 리액트 버전 16.8에서 추가 됨

1.1 리액트 훅이란?

함수형 컴포넌트에서도 클래스형 컴포넌트의 기능을 사용할 수 있게 하는 기능 훅을 통해서 컴포넌트의 상탯값을 관리 할 수 있고, 컴포넌트의 생명 주기 함수를 이용할 수 있다.

  1. 로직을 재사용하는 기존 방식의 한계

     * 리액트에서 로직의 재사용은 주로 '고차 컴포넌트', '렌더 속성값 패턴' 으로 이루어 진다.
     * 두 방법은 대상이 되는 컴포넌트를 감싸는 새로운 컴포넌트를 생성하기 때문에 리액트 요소 트리가 깊어진다.
     * 트리가 깊어지면 성능에 부정적 영향, 개발시 디버깅을 힘들게 하는원인이 된다.
  2. 클래스 형 컴포넌트의 한계

     * 서로 연과성이 없는 여러 가지 로직을 하나의 생명 주기 메서드에 작성하는 경우가 많다. (훅에서는 로직별로 구분이 가능하다 코드 5-10 참고)
     * componentDidMount 메서드에서 등록하고  componentWilUnmoun메서드에서 해제하는 코드가 자주 사용되는데, 등록하고 해제하는것을 깜빡하는 실수를 하기 쉽다.
     * 함수형 컴포넌트에 상탯값이나 생명주기 함수를 추가 하기 위해 클래스형 컴포넌트로 변경하는 작업도 상당히 귀찮은 작업!
     => 이유는 클래스형 컴포넌트를 작성할 때 부수적으로 작성해야 하는 코드가 많다.
     * 클래스 사용시 코드 압축이 잘 안되는 경우가 있고, 핫 리로드에서 난해한 버그를 발생시키고, 컴파일 단계에서 코드를 최적화 하기 어렵게 만든다.
  3. 훅의 장점

     * 훅은 함수 -> 함수 안에서 다른 함수 호출 가능 -> 새로운 훅 만들 수 있다.
     * 리액트 내장 훅과 다른 사람들이 만든 커스텀 훅을 조립해서 새로운 훅을 만들 수 있다.
     * 같은 로직을 한곳으로 모을 수 있어서 가독성이 좋다.
     (클래스형 컴포넌트의 생명 주기 메서드는 서로 다른 로직이 다른 로직이 하나의 메서드에 섞여 있어서 가독성이 좋지 않다. 또는 같은 로직이 componentDidMount와 componentdidUpdate 메서드에 중복으로 들어가기도 한다.)
     * 함수이기 때문에 클래스형 컴포넌트보다 타입스크립트로 정적 타입 언어 타입을 정의하기 쉽다.

1.2 함수형 컴포넌트에 상탯값 추가하기: useState

  1. useState 기본예 코드 5-1 useState 훅 사용하기
  2. ⭐️ 컴포넌트 상탯값 관리
  3. ⭐️ onChange 속성값으로 입력되는 함수는 렌더링이 될 때 마다 생성되므로 성능이 걱정됨으로 'useCallback' 훅 제공
  4. 5-24, 25 예제 참고

    • 코드 5-24 useCallback 훅이 필요한 예
    • 코드 5-25 useCallback 훅 사용하기
import React, { useState } from "react"
export default () => {
  const [name, setName] = useState("")
  return (
    <div>
      <h1> 예제 </h1>
      <p>{`# useState 테스트 - 나의 이름은 ${name}`}</p>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
    </div>
  )
}
  1. 클래스형 컴포넌트와 비교하기

    • 코드 5-2 클래스형 컴포넌트에서 상태값 관리하기
    • POINT1 초기화: 클래스 멤버 변수로 초기 상탯값을 정의
    • POINT2 상탯값 클래스 인스턴스에서 가져옴
    • POINT2 상탯값 변경시 'this.setState' 호출
        import React from 'react';
        export default class Profile extends React.Component {
          // POINT1 초기화: 클래스 멤버 변수로 초기 상탯값을 정의
          state = {
            name: ''
          };
    
          render() {
            // POINT2 상탯값 클래스 인스턴스에서 가져옴
            const { name } = this.state;
            return (
              <div>
                <h1> 예제 </h1>
                <p>{`# 클래스 컴포넌트 - 나의 이름은 ${name}`}</p>
                <input
                  type='text'
                  value={name}
                  //POINT2 상탯값 변경시 'this.setState' 호출
                  onChange={(e) => this.setState({ naem: e.target.value })}
                />
              </div>
            );
          }
        }
  2. 여러 개의 useState 훅 사용하기

    • 코드 5-3 여러 개의 useState 훅사용하기
    • 필요한 만큼 useState 훅을 호출 할 수 있다.
        import React, { useState } from 'react';
        export default () => {
          const [ name, setName ] = useState('');
          const [ age, setAge ] = useState(0);
          return (
            <div>
              <h1> 예제 </h1>
              <p>{`나의 이름은 ${name}, 나이는 ${age}`}</p>
              이름 입력: <input type='text' value={name} onChange={(e) => setName(e.target.value)} />
              <br />
              나이 입력: <input type='text' value={age} onChange={(e) => setAge(e.target.value)} />
            </div>
          );
        };
  3. useState 훅 하나로 여러 상탯값 관리하기

    • 코드 5-4 하나의 useState 훅으로 여러 상탯값 관리하기
    • 두 상태 값을 하나의 객체로 관리 가능
    • ⭐️ setState메서드는 기존 상탯값과 입력된 값을 병합하지만
    • ⭐️ useState 훅은 이전 상탯값을 지기 때문에 "...state"와 같은 코드가 필요.
    • ⭐️ 상탯값을 하나의 객체로 관리하는 경우를 위해 'useReducer' 훅이 제공
        import React, { useState } from 'react';
        export default () => {
          const [ name, setName ] = useState('');
          const [ age, setAge ] = useState(0);
          return (
            <div>
              <h1> 예제 </h1>
              <p>{`나의 이름은 ${name}, 나이는 ${age}`}</p>
              이름 입력: <input type='text' value={name} onChange={(e) => setName(e.target.value)} />
              <br />
              나이 입력: <input type='text' value={age} onChange={(e) => setAge(e.target.value)} />
            </div>
          );
        };

1.3 함수형 컴포넌트에서 생명 주기 함수 이용하기: useEffect

useEffect 두번째 매개변수 모양에 따른 3가지 의미
  1. ⭐️ useEffect 두번째 매개변수 '[]'의 의미
    - 컴포넌트가 마운트 될 때(=componentDidMounted) 첫번째 매개변수 함수 호출
    - 컴포넌트가 언마운트 될 때(=componentWillUnmount) 첫번째 매개변수 함수 호출

  2. ⭐️ useEffect 두번째 매개변수 '[value]'의 의미
    - 배열의 값이 변경되는 경우에만 함수가 호출

  3. ⭐️ useEffect 두번째 매개변수가 없을 때
    - 클래스 컴포넌트 "componentDidMount", "componentDidUpdate"두 함수에서 수행하는 것과같다.
  1. useEffect 기본 예 코드 5-5 useEffect 훅의 사용예
  2. ⭐️ useEffect에 있는 코드는 클래스형 컴포넌트에서 어떻게 구현할까?

    • 클래스형 컴포넌트의 componentDidMount, componentDidUpdate 양쪽 메서드에 추가하면 가은 기능을 하게 된다.
  3. ⭐️ 버튼을 누르면?

    • 다시 렌더링 되고, 렌더링이 끝나면 useEffect 훅에 입력된 함수가 호출
  import React, { useState, useEffect } from 'react';
  export default () => {
    const [ count, setCount ] = useState(0);
    //POINT
    useEffect(() => {
      document.getElementById('count').innerHTML = `업데이트 횟수: ${count}`;
    });

    return (
      <div>
          <h1> 예제 </h1>
          <div>
            <label id='count' />
          </div>
          <button onClick={() => setCount(count + 1)}>increase</button>
        </div>
      </div>
    );
  };
  1. API를 호출하는 기능(함수형 컴포넌트) 코드 5-6 useEffect 훅에서 API 호출하기
  2. usdEffect 훅의 두번째 매개변수로 배열을 입력하면, 배열의 값이 변경되는 경우에만 함수가 호출
import React, { useState, useEffect } from "react"
import axios from "axios"
function Profile({ userId }) {
  const [user, setUser] = useState(null)
  useEffect(() => {
    axios("https://api.github.com/users/happyjy").then(Response => {
      console.log(Response)
      setUser(Response.data)
    })
  }, [userId])
  return (
    <div>
      <div>
        <h1> 예제 </h1>
        <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div>
        <div>
          {user && (
            <p>
              {user.name}/ {user.id}
            </p>
          )}
        </div>
      </div>
    </div>
  )
}
export default Profile
  1. API를 호출하는 기능(클래스형 컴포넌트)

    • 코드 5-7 클래스형 컴포넌트에서 API 호출하기
    • 코드 5-6을 클래스형 컴포넌트로 작성
    • 첫번째 렌더링 후에는 componentDidMount메서드가 호출, 두번째 렌더링 후부터는 componentDidUpdate 메서드가 호출 되므로 API와 통신하는 코드를 양쪽에서 작성해야한다.
    • 단 componentDidUpdate 메서드에서는 userId가 변경된 경우에만 API를 호출
    • 이 처럼 클래스형 컴포넌트에서는 중복된 코드가 여러 생명주기 메서드에 흩어져 있는 경우가 많다.
      import React from 'react';
      import axios from 'axios';
      class Profile extends React.Component {
        state = {
          user: null
        };
        componentDidMount() {
          const { userId } = this.props || { userId: 'happyjy' };
          axios('https://api.github.com/users/happyjy').then((Response) => {
            console.log(Response);
            this.setState({ user: Response.data });
          });
        }
        componentDidUpdate(prevProps) {
          const { userId } = this.props || { userId: 'happyjy' };
          if (userId !== prevProps.userId) {
            axios('https://api.github.com/users/happyjy').then((Response) => {
              console.log(Response);
              this.setState({ user: Response.data });
            });
          }
        }
        render() {
          const { user } = this.state;
          return (
            <div>
              <div>
                <h1> 예제 </h1>
                <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div>
                <div>
                  {user && (
                    <p>
                      {user.name}/ {user.id}
                    </p>
                  )}
                </div>
              </div>
            </div>
          );
        }
      }
      export default Profile;
  2. 이벤트 처리 함수를 등록하고 해제하는 기능: 함수형 컴포넌트로 작성하기

    • 코드 5-8 useEffect 훅을 이용해서 이벤트 여러 함수를 등록하고 해제하기
    • POINT2: useEffect 첫번째 매개변수

      • ⭐️ useEffect 첫번째 매개변수에 등록된 함수가 함수를 반환할 수 있다.
      • ⭐️ 반환된 함수는 컴포넌트가 언마운트 되거나 첫번째 매개변수로 입력된 함수가 호출 되기 직전에 호출된다.
    • POINT3: useEffect 두번째 매개변수

      • ⭐️ 빈 배열을 넣으면 컴포넌트가 마운트 될 때, 컴포넌트가 언마운트 될때 첫번째 매개변수로 입력된 함수가 호출
        ⭐️ 클래스형 컴포넌트의 componentDidMount, componentWillUnmount 메서드에서만 실행되는 것과같은 효과가 있다.
    import React, { useState, useEffect } from "react"
    export default function() {
      const [width, setWidth] = useState(window.innerWidth)
      useEffect(() => {
        const onResize = () => setWidth(window.innerWidth)
        window.addEventListener("resize", onResize) //POINT1:
        // POINT2:
        return () => {
          window.removeEventListener("resize", onResize)
        }
      }, []) // POINT3:
      return (
        <div>
          <div>
            <h1> 예제 </h1>
            {`width is ${width}`}
          </div>
        </div>
      )
    }
  3. 이벤트 처리 함수의 등록하고 해제하는 기능: 클래스형 컴포넌트로 작성하기

    • 코드 5-9 이벤트 처리 함수의 등록과 해제를 클래스형 컴포넌트로 작성하기
        import React from 'react';
        class WidthPrinter extends React.Component {
          state = {
            width: window.innerWidth
          };
          componentDidMount() {
            window.addEventListener('resize', this.onResize);
          }
          componentWillMount() {
            window.removeEventListener('resize', this.onResize);
          }
          onResize = () => {
            this.setState({ width: window.innerWidth });
          };
          render() {
            const { width } = this.state;
            return (
              <div>
                <div>
                  <h1> 예제 </h1>
                  {`width is ${width}`}
                </div>
              </div>
            );
          }
        }
        export default WidthPrinter;
  4. 두 가지 기능을 합치기: 함수형 컴포넌트에서 작성하기

    • 코드 5-10 훅을 사용하면 로직별로 코드를 모을 수 있다.
    • 코드 5-06, 08 API호출, windowWidth 코드를 합쳐보자!
    • ⭐️ 각각의 로직이 다른 useEffect에 있어 가독성이 좋다.
        import React, { useState, useEffect } from 'react';
        import axios from 'axios';
        export default ({ userId = 'jyoon ' }) => {
          const [ user, setUser ] = useState(null);
          useEffect(
            () => {
              axios('https://api.github.com/users/happyjy').then((Response) => {
                console.log(Response);
                setUser(Response.data);
              });
            },
            [ userId ]
          );
          const [ width, setWidth ] = useState(window.innerWidth);
          useEffect(() => {
            const onResize = () => setWidth(window.innerWidth);
            window.addEventListener('resize', onResize); //POINT1:
            // POINT2:
            return () => {
              window.removeEventListener('resize', onResize);
            };
          }, []); // POINT3:
          return (
            <div>
              <div>
                <h1> 예제 </h1>
                <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div>
                <div>
                  {user && (
                    <p>
                      {user.name}/ {user.id}
                    </p>
                  )}
                </div>
                <div>{`width is ${width}`}</div>
              </div>
            </div>
          );
        };
  5. 클래스형 컴포넌트에서는 로직이 분산된다.

    • 코드 5-11 클래스형 컴포넌트에서는 로직이 분산된다.
        import React from 'react';
        import axios from 'axios';
    
        class Profile extends React.Component {
          state = {
            user: null,
            width: window.innerWidth
          };
          componentDidMount() {
            const { userId } = this.props || { userId: 'happyjy' };
            axios('https://api.github.com/users/happyjy').then((Response) => {
              console.log(Response);
              this.setState({ user: Response.data });
            });
    
            window.addEventListener('resize', this.onResize);
          }
          componentDidUpdate(prevProps) {
            const { userId } = this.props || { userId: 'happyjy' };
            if (userId !== prevProps.userId) {
              axios('https://api.github.com/users/happyjy').then((Response) => {
                console.log(Response);
                this.setState({ user: Response.data });
              });
            }
          }
          componentWillMount() {
            window.removeEventListener('resize', this.onResize);
          }
          onResize = () => {
            this.setState({ width: window.innerWidth });
          };
          render() {
            const { user, width } = this.state;
            return (
              <div>
                  <h1> 예제 </h1>
                  <div>{!user && <p>사용자 정보를 가져오는 중 ...</p>}</div>
                  <div>
                    {user && (
                      <p>
                        {user.name}/ {user.id}
                      </p>
                    )}
                  </div>
                  <div>{`width is ${width}`}</div>
                </div>
              </div>
            );
          }
        }
        export default Profile;

1.4 훅 직접 만들기

훅을 직접 만들어서 사용하면 고차 컴포넌트와 렌더 속성값 패턴 처럼 로직을 재사용할 수 있다.

  1. useWindowWidth 커스텀 훅

    • 코드 5-12 useWindowWidth 커스텀 훅
      import React, { useState, useEffect } from 'react';
      import useWindowWidth from './customHook/useWindowWidth';
    
      export default () => {
        const windowWidth = useWindowWidth();
        return (
          <div>
            <div>
              <h2> 결과 </h2>
              <p>this is widowWidth: {windowWidth}</p>
              <p>this is widowWidth: {windowWidth}</p>
            </div>
          </div>
        );
      };
    • POINT1

      • ⭐️ useState, useEffect훅을 이용해서 커스텀 훅을 만들었다.
      • ⭐️ 이렇게 레고 블록처럼 기존 훅을 이용해서 새로운 훅을 만들 수 있다는 점이 훅의 매력이다.
    • POINT2

      • ⭐️ 창의 너비를 저장해서 제공하는 것이 useWindowWidth 훅의 역할
  2. useWindowWidth 훅 사용하기

    • 코드 5-13 useWindowWidth 훅 사용하기
    • 창너비 조절시 다시 렌더링되어 화면에 나타난다.
    • ⭐️ 커스텀 훅은 리액트의 내장 훅과 같은 방식으로 사용될 수 있다.
    • 커스텀 훅과 내장 훅을 함께 사용하는 것도 가능
      import React, { useState } from 'react';
      import useWindowWidth from './customHook/useWindowWidth';
      export default function() {
        const width = useWindowWidth();
        const [ name, setName ] = useState('');
        return (
          <div>
            <div>
              <h1> 예제 </h1>
              <p>{`name is ${name}`}</p>
              <p>{`winddow width is ${width}`}</p>
              {width < 600 && <div>width가 600보다 작아요</div>}
              {width >= 600 && <div>width가 600보다 크거나 같아요</div>}
              <input type='text' value={name} onChange={(e) => setName(e.target.value)} />
            </div>
          </div>
        );
      }
    • ⭐️ 예로 부터 useWindowWidth 훅에 사용으로 미루어 커스텀 훅의 장점

      • 공통된 동작을 따로 빼서 다른 컴포넌트에도 똑같이 적용할 수 있겠다.
    • ⭐️ componentDidMount 시점부터 적용하기기 위해서는 어떻게 해야할까?
  3. useHasMounted 커스텀 훅

    다음 두 코드 컴포넌트 mount여부에 따라서 구현한 커스텀 훅, 고차 컴포넌트를 비교 했을때 두가지 비교사항이 있다.

    • 코드 5-14 useHasMounted 커스텀 훅
    • 코드 5-15 withHasMounted 고차 컴포넌트
    • 훅이 고차 컴포넌트보다 간결하게 코드 작성가능
    • 타입스크립트 같이 정적 타입 언어를 사용하는 경우 고차 컴포넌트의 타입을 정의하는 것이 여간 귀찮은 작업!, but 훅은 일반 함수이기 때문에 쉽게 타입을 정의할 수 있다.
    * 코드 5-14 useHasMounted 커스텀 훅
    ```js
        import React, { useState, useEffect } from "react";
        import useHasMounted from "./customHook/useHasMounted";
    
        export default function () {
          const hasMounted = useHasMounted();
          return (
            <div>
              <div>
                <h1> 예제 </h1>
                <p>{`this is hasMounted: ${hasMounted}`}</p>
              </div>
            </div>
          );
        }
    ```
      * 컴포넌트 마운트 여부를 알려주는 훅이다
      * ⭐️ useHasMounted 훅은 컴포넌트가 마운트 된 후에 참 반환
      * ⭐️⭐️ useEffect 훅의 두 번째 매개변수에 빈 배열을 넣었기 때문에 업데이트 하는 경우에는 setHasMounted 함수가 호출 되지 않는다.
      * ⭐️ useEffect 두번째 매개변수 '[]'의 의미
        - `컴포넌트가 마운트 될 때`, `컴포넌트가 언마운트 될 때` 첫번째 매개변수로 입력된 함수가 호출함수 return 함수 호출
      * ⭐️ useEffect 두번째 매개변수 '[value]'의 의미
        - 배열의 값이 변경되는 경우에만 함수가 호출
      * ⭐️ useEffect 두번째 매개변수가 없을 때
        - 클래스 컴포넌트 "componentDidMount", "componentDidUpdate"두 함수에서 수행하는 것과같다.
    
    * 코드 5-15 withHasMounted 고차 컴포넌트
      ```js
        import React, { Component } from "react";
        function withHasMounted(InputComponent) {
          return class OutputComponent extends Component {
            state = {
              hasMounted: false,
            };
            componentDidMount() {
              setTimeout(() => {
                this.setState({ hasMounted: true });
              }, 1000);
            }
            render() {
              const { hasMounted } = this.state;
              return (
                <InputComponent
                  {...this.props}
                  hasMounted={hasMounted}
                ></InputComponent>
              );
            }
          };
        }
        class Test extends Component {
          state = {};
          render() {
            console.log(this.props); //AppHook에서 넘겨준 value + withHasMounted에서 받은 props.
            const { hasMounted } = this.props;
            return (
              <div>
                <div>
                  <h1> 예제 </h1>
                  {!hasMounted && <div>마운트 되기 전입니다. 1초뒤 마운트 됩니다.</div>}
                  {hasMounted && <div>1초뒤 마운트 됐다 !!!</div>}
                </div>
              </div>
            );
          }
        }
        export default withHasMounted(Test);
      ```
    * 5-14 hook으로 되어 있는 기능을 "고차 컴포넌트로 작성한 코드"
    * ⭐️ 타입 스크립트와 같은 정적 타입 언어를 사용하는 경우
      - 고차 컴포넌트의 타입을 정의하는 작업 -> 손이 많이감.
      - 반면 훅은 일반 함수 -> 쉽게 타입 정의 가능

1.5 훅 사용 시 지켜야 할 규칙

  • 코드 5-16 훅 사용 시 규칙을 위한반 경우

    import React, { useState } from "react"
    export default function() {
    const [value, setValue] = useState(0)
    if (value === 0) {
      const [v1, setV1] = useState(0)
    } else {
      const [v1, setV1] = useState(0)
      const [v2, setV2] = useState(0)
    }
    
    return (
      <div>
        <p> 코드 5-16 훅 사용 시 규칙을 위한반 경우</p>
      </div>
    )
    }
    • ⭐️ 1. 조건에 따라 호출하면
    • 에러는 안나는데 순서가 보장 되지 않는다.
    • ⭐️ 2. for문 안에서는 아래와 같은 에러를
    • React Hook "useState" may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render
    • ⭐️ 3. 일반 function 안에서 호출할 시
    • React Hook "useState" is called in function "func" which is neither a React function component or a custom React Hook function
  • 훅의 호출 순서가 같아야 하는이유
  • 코드 5-17 여러 개의 훅 사용하기

    • ⭐️ 아래 예제로 "훅의 호출 순서가 같아야 하는 이유"를 알아보자.
    import React, { useState, useEffect } from "react"
    export default function() {
    const [age, setAge] = useState(0) //POINT1
    const [name, setName] = useState("") //POINT2
    
    useEffect(() => {
      setAge(31) //POINT3
    }, [])
    
    return (
      <div>
        <h1>예제</h1>
        this is age: {age}, this is name: {name}
      </div>
    )
    }
    • ⭐️ useState훅에 전달한 정보는 상탯값의 기본값밖에 없다.
      => ⭐️ 리액트가 age, name상탯값을 구분할 수 있는 유일한 정보는 훅이 사용된 순서
    • 추측!
    • ⭐️ useState하면 생성되는 데이터 들이 stack 으로 관리 된다라고 추측
    • 예제 5-18 리액트 내부코드 예제를 보면 배열로 관리 되고 있다.
  • 리액트가 내부적으로 훅을 처리하는 방식
  • 코드 5-18 의사코드로 표현한 리액트의 내부 코드

    import React from "react"
    export function useHook() {
    //POINT1
    //...
    const hookData = {} //임의
    useHook.push(hookData) //POINT2
    }
    
    //POINT3
    function process_a_component_rendering(component) {
    hooks = [] //POINT4
    component() //POINT5
    let hooksForThisComponent = hooks //POINT6
    hooks = null //POINT6
    //...
    }
    
    export default function() {
    return (
      <div>
        <p> 코드 5-18 의사코드로 표현한 리액트의 내부 코드</p>
      </div>
    )
    }
    • POINT1
    • 리액트가 내장하고 있는 useState, useEffect와 같은 훅
    • POINT2
    • ⭐️ 각 훅 함수에서는 hooks 배열에 자신이 데이터를 추가
    • POINT3
    • 렌더링 과정에서 하나의 컴포넌트를 처리하는 함수
    • POINT4
    • hooks를 빈 매열로 초기화
    • POINT5
    • 컴포넌트 내부에서 훅을 사용한 만큼 hooks 배열에 데이터가 추가
    • POINT6
    • 생성된 배열을 저장하고 hooks 변수를 초기화 한다.
    • 이처럼 사용된 순서를 저장하고 배열에 저장된 수서를 기반으로 훅을 관리