모던 리액트 2주차 스터디 발표자료 - 3장

강석우's avatar
Mar 16, 2024
모던 리액트 2주차 스터디 발표자료 - 3장

훅의 규칙

  • 최상위에서만 훅을 호출해야 한다. 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.

  • 훅을 호줄할 수 있는 것은 리액트 함수 컴포넌트, 훅 또는 사용자 정의 훅 두 가지 뿐이다.

useState

const [test, setTest] = useState<string>('');

게으른 초기화

localStorage 또는 sessiong Storage에 대한 접근, map, filter, find 같은 배열에 대한 접근 혹은 초깃값 계산을 위한 함수 호출이 필요한 경우 사용

const [test, setTest] = useState(Number.parseInt(window.localStorage.getItem(cachekey))

useEffect

첫번째 인수로 실행함수, 두번째 인수로 의존성 배열

useEffect(()=>{
 console.log('testingCode')
 window.addEventListener('click',addMouseEvent);
 return ()=>{
  window.removeEventListener('click',addMouseEvent);
 }
},[]);

클린업 함수

콜백이 실행 될 경우 이전의 콜백 존재 시 실행된다.
이전 콜백 존재 확인 ➡️ ( 존재 시 ) 클린업 함수 실행 ➡️ 현재 콜백 함수 실행

  • 이전 state값으로 실행된다.

  • 이벤트 핸들러의 무한 생성을 방지하는 목적으로 사용된다.

useEffect사용과 일반 선언차이

  • useEffect는 렌더링 후에 실행이 되기 때문에 클라이언트 사이드 실행을 보장해준다.
    사용하지 않을 경우 렌더링 중 실행이 될 수 있기 때문에 ssr의 경우에는 서버에서도 실행이 가능하다.

  • 클라이언트 사이드에서 실행되는 것이 보장되기 때문에 window 객체에 접근이 가능하다.

함수명 부여가 가능하다.

useEffect(
 function testFunction(){
   console.log('write to test this function')
 },[]
)

useMemo

비용이 큰 연산에 대한 결과를 저장하고 저장된 값을 반환하는 훅이다.
첫 번째 인수로 값을 반환하는 생성 함수를, 두 번째 인수로 해당 함수가 의존하는 값의 배열이 들어간다.

의존성 배열의 값이 변경 될 경우 첫 번째 인수의 함수를 실행 후에 그 값을 반환하고 그 값을 기억해준다. 변경되지 않을 경우에는 기억하고 있는 값을 그대로 전달해 준다.

useCallback

인수로 넘겨받은 콜백 자체를 기억한다.
첫 번째 인수로 함수를, 두 번째 인수로 의존성 배열이 들어간다.

렌더링 발생 시 의존성 배열이 변경 됐다면 함수를 재생성하고 그 함수를 기억한다.
변경되지 않았다면 기존 함수를 그대로 반환한다.

useRef

useState와 마찬가지로 렌더링 후에 변경 가능한 값을 저장할 수 있다.

  • useRef는 값이 변하더라도 렌더링을 발생시키지 않는다.

  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경이 가능하다.

가장 일반적인 사용 예는 DOM에 접근하고 싶을 때이다.
useRef의 최초 기본 값은 return문에 정의해둔 DOM이 아니고 useRef()로 넘겨받은 인수이다. 따라서 useRef가 선언된 당시에는 컴포넌트가 존재하지 않으므로 undefined가 된다.

useContext

Context란?

리액트에서의 정보 전달 방식에는 prop drilling 이 있는데 최상단 컴포넌트에서 최하단 컴포넌트까지 정보를 전달하기에는 너무 많은 계층을 소요하기 때문에 나온 개념이다.
 

useContext 훅

상위 컴포넌트에서 사용된 provider의 값을 아래의 컴포넌트에서 사용할 수 있게 된다.

const Context = createContext<{ hello: string } | undefined>(undefined)
function ParentComponent() {
 return (
 <>
   <Context.Provider value={{ hello: 'react' }}>
    <Context.Provider value={{ hello: 'javascript' }}>
     <ChildComponent />
    </Context.Provider>
   </Context.Provider>
 </>
 )
}
function ChildComponent() {
 const value = useContext(Context)

 return <>{value ? value.hello : ''}</>
}

개인적 견해

상태 주입이 주 목적일 뿐더러 컴포넌트를 context로 계속 감싸주어야 하는데 깔끔하게 사용하기 어려울 것 같다.

useReducer

useState와 비슷하지만 더 복잡한 상태 값을 미리 정해놓은 시나리오에 맞게 관리할 수 있다.

인자

  • reducer : useReducer의 기본 action을 의미하는 함수다. 첫번째 인수.

  • initialState : 두번 째 인수로 useReducer의 초기값을 의미한다.

  • init : 게으른 초기화를 시키는 인수이다.

반환값

  • state : 현재 useReducer가 가지고 있는 값

  • dispatcher : state를 업데이트하는 함수.

import { useReducer } from "react";

// useReducer가 사용할 state를 정의
type State = {
  count: number;
};
type Action = { type: "up" | "down" | "reset"; payload?: State };

// 초깃값
const initialState: State = { count: 0 };

// 앞서 선언한 state와 action을 기반으로 state가 어떻게 변경될지 정의
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "up":
      return { count: state.count + 1 };
    case "down":
      return { count: state.count - 1 > 0 ? state.count - 1 : 0 };
    case "reset":
      return init(action.payload || { count: 0 });
    default:
      throw new Error(`Unexpected action type ${action.type}`);
  }
}
// 무거운 연산이 포함된 게으른 초기화 함수
function init(count: State): State {
  // count: State를 받아서 초깃값을 어떻게 정의할지 연산하면 된다.
  return count;
}

export default function App() {
  const [state, dispatcher] = useReducer(reducer, initialState, init);

  function handleUpButtonClick() {
    dispatcher({ type: "up" });
  }
  function handleDownButtonClick() {
    dispatcher({ type: "down" });
  }
  function handleResetButtonClick() {
    dispatcher({ type: "reset", payload: { count: 1 } });
  }

  return (
    <div className="App">
      <h1>{state.count}</h1>
      <button onClick={handleUpButtonClick}>+</button>
      <button onClick={handleDownButtonClick}>-</button>
      <button onClick={handleResetButtonClick}>reset</button>
    </div>
  );
}

장점 : 복잡한 계산이 자주 일어나거나 여러 state에 대한 연산이 이루어져야 할 때 사용된다.

useImperativeHandle

useImperativeHandle 이란 부모에게서 넘겨받은 ref를 수정할 수 있는 훅이다.

forwardRef

forwardRef는 ~~ref를 ref 라는 이름으로 상위 컴포넌트에서 하위컴포넌트로 prop drilling을 통해 내려주려 할 때 에러가 나는 것을 방지하기 위해 만들어졌다.

function ChildComponent({ ref }) {
  useEffect(() => {
    // undefined
    console.log(ref);
  }, [ref]);
  return <div>안녕!</div>;
}

function ParentComponent() {
  const inputRef = useRef();
  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef} />
    </>
  );
}

위의 예시를 보면 ChildComponent에 inputRef 를 ref 로 넣어주려 하면 에러가 난다.
이는 forwardRef를 사용하면 해결이 된다.

const ChildComponent = forwardRef((props, ref) => {
  useEffect(() => {
    // {current: undefined}
    // {current: HTMLInputElement}
    console.log(ref);
  }, [ref]);
  return <div>안녕!</div>;
});

function ParentComponent() {
  const inputRef = useRef();
  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef} />
    </>
  );
}

forwardRef를 실무에서 사용한 적이 있다. ref 로 prop drilling 을 했을 때 에러가 발생해 그저 타입스크립트 에러인줄 알고 구글링을 해가며 forwardRef를 사용했었는데 이런 원리도 모른채 사용했었다. 이제는 그럴일 없게 공부하자.

useImperativeHandle(
    ref,
    () => ({
      alert: () => alert(props.value),
    }),
    // useEffect의 deps와 같다.
    [props.value]
  );

위의 useImperativeHandle은 ref를 하위 컴포넌트에 받아왔을때 정보을 받기만 하는 것이 아니라 추가적인 동작을 추가 할 수있게 해준다.

useLayoutEffect

이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다.

  1. 리액트가 DOM을 업데이트

  2. useLayoutEffect를 실행

  3. 브라우저에 변경 사항을 반영

  4. useEffect를 실행

언제 사용해야 할까?

특정 요소에 따라 DOM 요소를 기반으로 한 애니메이션, 스크롤 위치를 제어하는 등 화면에 반영되기 전에 하고 싶은 작업에 useLayoutEffect를 사용한다면 useEffect를 사용했을 때보다 훨씬 더 자연스러운 사용자 경험을 제공할 수 있다.

useDebugValue

리액트 애플리케이션을 개발하는 과정에서 사용된다.
디버깅 하고싶은 정보를 이 훅에다 사용하면 리액트 개발자 도구에서 볼 수 있다.

  • 사용자 정의 훅 내부의 내용에 대한 정보를 남기는 훅이다.

    두 번째 인수로 포매팅 함수를 전달하면 이에 대한 값이 변경될 때만 포매팅된 값을 노출한다.

  • 오직 다른 훅 내부에서만 실행할 수 있다.

사용자 정의 훅

서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것
반드시 네이밍을 할 때 앞에 use를 붙여야 한다.

고차 컴포넌트

고차함수의 일종으로 리액트 뿐만 아니라 자바스크립트 환경에서 널리 쓰일 수 있다.

고차함수란?

함수를 인수로 받거나 결과로 반환하는 함수이다. ex ) map, forEach, reduce 등

고차 컴포넌트 예시

인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도로 정의된 공통 컴포넌트를 보여주는 시나리오

interface LoginProps {
  loginRequired?: boolean;
}

function withLoginComponent<T>(Component: ComponentType<T>) {
  return function (props: T & LoginProps) {
    const { loginRequired, ...restProps } = props;
    if (loginRequired) {
      return <>로그인이 필요합니다.</>;
    }
    return <Component {...(restProps as T)} />;
  };
}

// 원래 구현하고자 하는 컴포넌트를 만들고, withLoginComponent로 감싸기만 하면 끝이다.
// 로그인 여부, 로그인이 안 되면 다른 컴포넌트를 렌더링하는 책임은 모두
// 고차 컴포넌트인 withLoginComponent에 맡길 수 있어 매우 편리하다.
const Component = withLoginComponent((props: { value: string }) => {
  return <h3>{props.value}</h3>;
});

export default function App() {
  // 로그인 관련 정보를 가져온다.
  const isLogin = true;
  return <Component value="text" loginRequired={isLogin} />;
  // return <Component value="text" />;
}

고차 컴포넌트 특징

  • 리액트의 고차 컴포넌트도 마찬가지로 with로 시작하는 이름을 사용해야 한다.
    강제되는 것은 아니지만 다들 그렇게 사용하고 있으니 맞춰서 사용하도록 하자.

  • 고차 컴포넌트를 사용할 때 주의할 점 중 하나는 부수 효과를 최소화해야 한다.

  • 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커진다.

사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?

사용자 정의 훅이 필요한 경우

단순히 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 것이 좋다.

고차 컴포넌트가 필요한 경우

함수 컴포넌트의 반환값, 즉 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다

Share article

석우의 개발블로그