패키지 업데이트(기존 react-scripts 4.0x 버전에서 5.0버전으로)
yarn add react-scripts@latest
<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 메서드 사용)
바닐라 틸트 패키지 설치
참고: 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 에서 플러그인 인스턴스 파괴 및 이벤트를 해제해준다.
참고: 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
을 전달하는 방법도 있다.
참고 사이트: https://ko.reactjs.org/docs/portals.html
리액트 앱 바깥에 React 컴포넌트를 렌더링 하는 것 --> 대표적인 케이스: Dialog, Notification
키보드 포커스 관리가 중요! (접근성을 준수하는 웹을 위해)
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/
Dialog안의 isVisible의 상태를 바꾸기 위해서는 Dialog 자신은 상태를 가지고 있지 않기 때문에 못 바꿈 따라서 Dialog => Box => App 까지 상위 컴포넌트로 이동해야됨
이러한 경우 이벤트를 props drilling을 통해 App => Box => Dialog 로 전달하여 작동시켜야함
Q. 그렇다면 Dialog에 상태를 주면 안되는 것인가 ?
--> 그렇다 Dialog가 직접 상태를 갖게되면 그 위의 상위 요소가 Dialog에 직접 접근할 수 있는 방법이 없고, 상태를 각 컴포넌트마다 갖고있게되어 어려워진다. (중앙 지휘소 한곳에 상태를 관리하는것이 편하다)
결국 props drilling의 문제를 해결하기 위해 상태관리 라이브러리를 사용한다.
참고 사이트: https://2021.stateofcss.com/ko-KR/technologies
사이트에 들어가서 최근 CSS의 만족도와 사용량을 보면 CSS Modules의 만족도가 꽤 높다는 것을 확인하할 수 있다.
그렇다면 CSS Modules는 어떻게 사용하는 가?
아래의 사이트를 참고하여 직접 설정을 구성해야 한다.
참고 사이트: https://webpack.kr/loaders/postcss-loader/#css-modules
참고 사이트: 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 이렇게 접근해야하기 때문에 케밥케이스보단 카멜케이스로 접근하여 스타일링 하게되며, 클래스명만 잘 주면 더욱 깔끔한 코드를 짤 수 있게 된다.