혼자 적어보는 노트

프로그래머스 데브코스 TIL - Day 55 본문

스터디

프로그래머스 데브코스 TIL - Day 55

jinist 2022. 6. 3. 23:38

 

✅ 오늘의 학습

📌 React (8)

 

- 사용자 정의 Hook 연습하기

useAsync / useHotKey

- 컴포넌트 연습하기

Modal / Toast

 


 

useAsync

이전의 setTimeout과 마찬가지로 함수버전과 즉시실행 버전을 만들었다.

 

로직을 구성하는 것은 어렵진 않으나 비동기 실행 시 중복 요청에 대한

결과 값을 마지막 요청의 것으로만 적용이 되게 방어 코드를 작성해야 한다.

 

const useAsyncFn = (fn, deps) => {
  const lastCallId = useRef(0);
  // 비동기 실행시 중복 요청에 대한 결과값을 마지막 요청의 것으로만 적용하기 위함.
  const [state, setState] = useState({
    isLoading: false,
  });

  const callback = useCallback((...args) => {
    const callId = ++lastCallId.current;

    if (!state.isLoading) {
      setState({ ...state, isLoading: true });
    }

    return fn(...args).then(
      (value) => {
        console.log(callId, lastCallId);
        callId === lastCallId.current && setState({ value, isLoading: false });
        return value;
      },
      (error) => {
        callId === lastCallId.current && setState({ error, isLoading: false });
        return error;
      },
    );
    // eslint-disable-next-line
  }, deps);

  return [state, callback];
};

lastCallId를 사용하여 마지막 요청의 id값을 저장하고

callback함수 내부에서 callId를 사용하여 들어온 요청에 따라 숫자를 증가시킨다.

 

연달아 요청을하면 lastCallId의 값이 계속 증가하게되어 누적된 값을 가지게 되고

callId의 경우 함수를 실행시켰을 시점의 값을 그대로 가지고 있기 때문에 lastCallId값과 일치하지 않아

앞부분 call을 무시할 수 있다.

 

 

useHotKey

키보드 단축키를 사용하기 위한 hook

 

전역에서 사용할 단축키와 특정 영역에서 사용할 단축키를 구분하여 사용할 수 있도록 구분해서 작성해 주었다.

 

 keyboard이벤트를 통해 동시에 입력된 키가 어떤 키인지 알 수 있었고

지정한 bitmasks값을 사용하여 어떤 버튼이 동시에 눌렸는지 구분을 할 수 있다는 것이 신기했다.

const ModifierBitMasks = {
  alt: 1,
  ctrl: 2,
  meta: 4,
  shift: 8,
};

 

그리고 리액트에서 이벤트를 내려줄 때는 native event가 아니라

래핑된 이벤트를 전달을 해준다는 것을 이번에 알게 되었다.

 

React 공식문서

 

공식문서를 살펴보니 리액트에서 이벤트의 인터페이스는 브라우저의 고유 이벤트와 같고

모든 브라우저에서 동일하게 동작되지만 래핑이 되어있다.

 

브라우저의 고유 이벤트가 필요하다면 nativeEvent 속성을 사용하여 활용할 수 있다.

 

 

 

Modal 컴포넌트

createPortal을 사용하여 body에 createElement로 생성한 el에 모달을 넣어줄 수 있다.

const Modal = ({
  children,
  width,
  height,
  visible = false,
  onClose,
  ...props
}) => {
  const containerStyle = useMemo(
    () => ({
      width,
      height,
    }),
    [width, height],
  );

  // 부모에서 onClose를 받아서 처리
  const ref = useClickAway(() => {
    onClose && onClose();
  });

  const el = useMemo(() => document.createElement('div'), []);
  // body에 모달 생성
  useEffect(() => {
    document.body.appendChild(el);
    return () => {
      document.body.removeChild(el);
    };
  });

  return ReactDOM.createPortal(
    <BackgroundDim style={{ display: visible ? 'block' : 'none' }}>
      <ModalContainer
        {...props}
        ref={ref}
        style={{ ...props.style, ...containerStyle }}
      >
        {children}
      </ModalContainer>
    </BackgroundDim>,
    el,
  );
};

export default Modal;

 

 

Potals

강의에서 처음 접해보는 부분이라 portal에 대해 알아보았다.

 

React 공식문서 : Portals

ReactDOM.createPortal(child, container)

 

첫 번째 인자는 엘리먼트나 렌더링할 수 있는 React의 자식요소를 넣을 수 있고

두 번째 인자는 컨테이너를 지정할 수 있다.

==> 두 번째 인자의 자식으로 첫 번째 인자가 들어가게 된다.

 

Component Tree 위치

createPortal을 사용하여 body 내부에 특정 컴포넌트를 위치시켰을 경우

dom의 위치는 body내부에 있지만 React의 컴포넌트 tree에서는 호출 위치의 하위에 위치하게 된다.

* Portal 엘리먼트는 modal의 자식이 마운트 된 이후에 DOM 트리에 삽입된다.

 

이벤트 버블링

 React의 컴포넌트 tree에서는 호출 위치의 하위에 위치하고 있기 때문에

부모이벤트에 이벤트 버블링 현상이 생기게된다.

 

Lifecycle

createPortal로 연결된 경우에도 컴포넌트의 생명주기와 합성 이벤트가 적용이 된다.

컴포넌트로 래핑하여 컴포넌트 내에서 lifecycle을 통해 state및 props의 변화를 전달시킬 수 있다.

그리고 lifeCycle에 의해 자동으로 unMount된다.

 

 

Toast 컴포넌트

작은 팝업형태로 알림을 띄워주는 컴포넌트

 

<button onClick={() => Toast.show('안녕하세요!', 3000)}>Show Toast</button>

버튼의 onClick이벤트를 통해 Toast의 알림 메세지와 메세지의 지속 시간을 전달하여 화면에 띄우는 방식이다.

 

Toast

ㄴindex.js

ㄴToastItem.js

ㄴToastManager.js

 

Toast의 경우 modal과 마찬가지로 body태그 내부에서 생성해야하는데

body태그 내부에서 Toast를 생성을 담당하는 index.js

bind를 통해 생성된 Toast내부에 ui를 넣고 빼는 것을 담당하는 ToastManager.js

Toast내부의 ui와 animation을 담당하는 ToastItem.js로 구성하여 만들었다.

 

[Toast/index.js]

import ReactDOM from 'react-dom';
import ToastManager from './ToastManager';

class Toast {
  portal = null;
  constructor() {
    const portalId = 'toast-portal';
    const portalElement = document.getElementById(portalId);

    if (portalElement) {
      this.portal = portalElement;
      return;
    } else {
      this.portal = document.createElement('div');
      this.portal.id = portalId;
      document.body.appendChild(this.portal);
    }
    ReactDOM.render(
      <ToastManager
        bind={(createToast) => {
          this.createToast = createToast;
          // show함수를 실행했을 때 토스트매니저 안의 createToast가 실행된다.
        }}
      />,
      this.portal,
    );
  }
  show(message, duration = 2000) {
    this.createToast(message, duration);
  }
}

export default new Toast();

클래스를 통해 Toast를 만들고 내부에서 ReactDom.render()를 통해 바로 렌더를 해줌으로써

컴포넌트 자체를 최상위에 작성하지 않고도 body태그 안에 위치시킬 수 있다.

 

createPortal와 비슷하지만 Portal과는 다르게 render를 사용하여 body내부에 위치시켰을 경우,

Dom 위치는 물론 React의 컴포넌트 트리에서도 최상위에 생성된다.

* 그리고 render는 unMount를 직접 해주어야한다.

 

 

💡 근데 여기서 잠깐!!

render를 사용하게되면 상단에 에러가 발생하게된다.

ReactDOM.render는 React 18에서 더 이상 지원되지 않아서 createRoot를 사용을 하라는 뜻!

 

React 공식문서 : Updates to Client Rendering APIs

 

이전에 아래와 같은 형태로 작성했다면

// before
import ReactDom from 'react-dom'; before

const container = document.createElement('div');
document.body.appendChild(container);
ReactDom.render(content, container); // before

 

이렇게 작성하면 된다.

// after
import { createRoot } from 'react-dom/client'; 

const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
root.render(content);

 

 


✍ 느낀 점

 

리액트 컴포넌트 만들기와 hook 만들기 파트가 끝났다!

자주 사용될만한 컴포넌트와 hook들을 만들어본 경험이 프로젝트를 진행할 때 많은 도움을 줄 듯 했다.

그리고 컴포넌트들을 조합해서 또 다른 컴포넌트를 만들어 보았는데

이래서 재사용성이 좋은 컴포넌트들을 만들어 놓으면 블록처럼 조합해서 쓰기가 좋다는 것을

몸소 깨닫게 되었다..ㅎ

 


Portal, Render 차이점

Comments