[React] - Ref, CSS Modules

Lee Jeong Min·2021년 12월 20일
0
post-thumbnail

컨테이너 컴포넌트

패키지 업데이트(기존 react-scripts 4.0x 버전에서 5.0버전으로)

yarn add react-scripts@latest

Ref를 사용하는 이유?

  • React 앱은 기본적으로 선언형 프로그래밍 사용이 권장되지만 모든 것을 선언형으로 작성할 수 없다.
  • 외부 라이브러리 통합 또는 애니메이션, DOM 스크립트와 같은 명령형 프로그래밍(실제 DOM 노드에 접근/조작)이 필요할 때가 있다.
  • 그러므로 렌더링 이후, DOM 요소에 접근/조작 해야한다. (라이프 사이클이 필요한 이유)
  • DOM 요소에 접근/조작 하려면 DOM API를 사용할 수 있지만, React는 ref을 통해 DOM에 접근/조작하는 방법을 제공한다.
  • 즉 React에서는 ref를 통해 렌더링 이후 DOM 요소를 통해 접근/조작할 필요가 있다.

콜백 ref

<div ref={(domNode) => console.log(domNode)} className="tiltCard">
  {children}
</div>

가상돔이 실제 돔에 렌더링 되었을 때, 인스턴스 멤버로 React 컴포넌트나 DOM 엘리먼트를 인자로 받는다.

참고: https://ko.reactjs.org/docs/refs-and-the-dom.html#callback-refs

setTiltCardNode = (domNode) => {
  this.tiltCardNode = domNode;
};

...

<div ref={this.setTiltCardNode} className="tiltCard">
  {children}
</div>

콜백 ref로 전달하는 함수는 화살표 함수로 작성하여 this가 클래스를 가리키게 만듦. (es5에서는 bind 메서드 사용)

JS 틸트 패키지

바닐라 틸트 패키지 설치

참고: https://www.npmjs.com/package/vanilla-tilt

yarn add vanilla-tilt 
yarn i -D @types/vanilla-tilt // 타입 인텔리센스
  componentDidMount() {
    const { current: tiltCardNode } = this.tiltCardRef;
    VanillaTilt.init(tiltCardNode, TiltCard.defaultProps.option);
  }

componentDidMount 사이클에 틸트 옵션 연결

이벤트 연결 후 해제

componentDidMount() {
  ...
  // TiltCard 플러그인이 연결된 DOM 노드에 설정된 커스텀 이벤트 감지
  tiltCardNode.addEventListener('tiltChange', ({ detail }) => {
    console.log(detail);
  });
}

componentWillUnmount() {
  const { current: tiltCardNode } = this.tiltCardRef;

  // 플러그인 인스턴스 파괴
  tiltCardNode.vanillaTilt.destroy();

  // 연결된 이벤트 해제(제거)
  tiltCardNode.removeEventListener('tiltChange', this.handleTiltChange);
}

componentDidMount 시 이벤트 연결 후, componentWillUnmount 에서 플러그인 인스턴스 파괴 및 이벤트를 해제해준다.

Forwarding Refs

참고: https://ko.reactjs.org/docs/forwarding-refs.html

ref 전달은 컴포넌트를 통해 자식 중 하나에 ref를 전달하는 기법

컴포넌트 밖에서 Ref 참조하려면 위를 사용하는데 이를 사용하는 이유는 접근성 때문!

ref를 props로 전달할 수 없어서 forwardRef api 사용한다.

A11yHidden.jsx

import './A11yHidden.css';
import { forwardRef } from 'react';
import { classNames } from 'utils';

export const A11yHidden = forwardRef(function (
  { as: Comp, className, focusable, children, ...restProps },
  ref
) {
  return (
    <Comp
      ref={ref}
      className={classNames('a11yHidden', className, { focusable })}
      {...restProps}
    >
      {children}
    </Comp>
  );
});

A11yHidden.defaultProps = {
  as: 'span',
};

TiltCardContainer.js

import { Component, createRef } from 'react';

...

a11yHidden = createRef(null);

    return (
      ...
        <A11yHidden ref={this.a11yHidden}>나는 보이지 않아요.</A11yHidden>
       ...
    );
...

async componentDidMount() {
  console.log(this.a11yHidden.current); // { current: span.a11yHidden }
  ...
}

forwarding api를 사용하는 방법도 있고, prop으로 innerRef, forwardRef을 전달하는 방법도 있다.

차원 이동 렌더링(Portal)

참고 사이트: https://ko.reactjs.org/docs/portals.html

리액트 앱 바깥에 React 컴포넌트를 렌더링 하는 것 --> 대표적인 케이스: Dialog, Notification

키보드 포커스 관리가 중요! (접근성을 준수하는 웹을 위해)

Dialog 실습

import './Dialog.css';
import React from 'react';
import { createPortal } from 'react-dom';

// 모달 다이얼로그 컴포넌트가 차원이동(portal) 렌더링 될 컨테이너 요소(실제 DOM 노드)
const dialogContainer = document.getElementById('modal-dialog-zone');

console.log(dialogContainer);

function renderDialog() {
  return (
    <div
      role="dialog"
      aria-modal="true"
      className="modalDialog"
      aria-hidden="true"
      aria-label="React Portal﹕모달 다이얼로그"
    >
      <div className="content">
        <h2>포털</h2>
        <p>여기가 React 앱 밖의 세상인가요?!</p>
      </div>
      <button
        type="button"
        className="closeDialogButton"
        aria-label="모달 다이얼로그 닫기"
        title="모달 다이얼로그 닫기"
      >
        <svg
          width="24"
          height="24"
          xmlns="http://www.w3.org/2000/svg"
          fillRule="evenodd"
          clipRule="evenodd"
        >
          <path d="M12 11.293l10.293-10.293.707.707-10.293 10.293 10.293 10.293-.707.707-10.293-10.293-10.293 10.293-.707-.707 10.293-10.293-10.293-10.293.707-.707 10.293 10.293z" />
        </svg>
      </button>
      <div role="presentation" className="dim" />
    </div>
  );
}

export const Dialog = ({ isVisible }) => {
  return isVisible ? createPortal(renderDialog(), dialogContainer) : null;
};

왜 Portal이 필요할까?
React App 내부에 종속된 컴포넌트는 overflow, position 등 스타일 속성에 의해 부모 컴포넌트 내부에 렌더링 될 경우 모달 다이얼로그 또는 노티피케이션 등 UI 적으로 화면 가장 상위에 띄워야 할 경우 문제가 된다.
따라서 React App 외부에 다이얼로그를 렌더링 해야 한다.
그러므로 이런 경우 Portal API를 사용해야한다.

접근 가능한 Dialog 생성 방법에 대한 참고 사이트: https://www.smashingmagazine.com/2021/07/accessible-dialog-from-scratch/

props drilling(드릴링)

Dialog안의 isVisible의 상태를 바꾸기 위해서는 Dialog 자신은 상태를 가지고 있지 않기 때문에 못 바꿈 따라서 Dialog => Box => App 까지 상위 컴포넌트로 이동해야됨

이러한 경우 이벤트를 props drilling을 통해 App => Box => Dialog 로 전달하여 작동시켜야함


Q. 그렇다면 Dialog에 상태를 주면 안되는 것인가 ?

--> 그렇다 Dialog가 직접 상태를 갖게되면 그 위의 상위 요소가 Dialog에 직접 접근할 수 있는 방법이 없고, 상태를 각 컴포넌트마다 갖고있게되어 어려워진다. (중앙 지휘소 한곳에 상태를 관리하는것이 편하다)

결국 props drilling의 문제를 해결하기 위해 상태관리 라이브러리를 사용한다.

CSS Modules

참고 사이트: https://2021.stateofcss.com/ko-KR/technologies

사이트에 들어가서 최근 CSS의 만족도와 사용량을 보면 CSS Modules의 만족도가 꽤 높다는 것을 확인하할 수 있다.

그렇다면 CSS Modules는 어떻게 사용하는 가?

  1. webpack을 직접 구성한 경우

아래의 사이트를 참고하여 직접 설정을 구성해야 한다.
참고 사이트: https://webpack.kr/loaders/postcss-loader/#css-modules

  1. CRA 환경인 경우

참고 사이트: https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/

css 파일을 [name].module.css 형식대로 만들어주면 사용할 수 있다.

SkHeading.module.css

.headline {
  text-transform: uppercase;
  margin: 40px 0 22px 16px;
  font-size: 2.4rem;
  font-weight: 900;
  line-height: 1;
}

.SK {
  display: block;
  margin-right: 0.25em;
  color: #de071a;
}

.title {
  color: #fc6620;
}

@media (min-width: 40em /* 640px */) {
  .headline__SK {
    display: inline;
  }
}

SkHeading.js


//module css
import styles from './SkHeading.module.css';

import React from 'react';
import { classNames } from 'utils';

console.log(styles); // {headline, SK, title: }

const SkHeading = ({
  as: Comp,
  isVisible,
  className,
  children,
  ...restProps
}) => {
  if (!children || typeof children !== 'string') {
    throw new Error(
      'SkHeading 컴포넌트의 children prop은 필수이며 문자 타입만 허용합니다.'
    );
  }
  return (
    <Comp className={classNames(styles.headline, className)} {...restProps}>
      <span className={styles.SK}>SK</span>
      <span className={styles.title}>{children.toUpperCase()}</span>
    </Comp>
  );
};

SkHeading.defaultProps = {
  as: 'h2',
  className: '',
  isVisible: true,
};

export default SkHeading;

css는 원래 스코프가 존재하지 않아 전역으로만 사용이 가능했지만, 지역 스코프 단위로 사용할 수 있게 된다.

또한 사용할 시에도 앞에 styles만 붙이고 클래스 명만 주면 BEM 방법론에서 사용하는 그렇게 긴 스타일 클래스명을 주지 않아도 되고, styles.xxx 이렇게 접근해야하기 때문에 케밥케이스보단 카멜케이스로 접근하여 스타일링 하게되며, 클래스명만 잘 주면 더욱 깔끔한 코드를 짤 수 있게 된다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글