요 근래 지속적으로 Java 11 + Spring 프로젝트만 진행하면서 다음과 같은 생각이 내 고민순위 중 하나로 등재되었다.
"계속 죽어가는 프런트엔드 능력을 살려둘 기회가 없을까"
프런트엔드 능력을 지속적으로 개발하기 위해 프로젝트를 하나 더 진행하는 건 부담이 꽤 크다.
그러던 나날 중 백엔드 개발 이슈에 꽉 막힌 나는 잠시 쉬어가기 위해 개발 관련 컬럼을 읽기 시작했고 FrontendPro 를 발견했다.
대충 둘러보니 해당 사이트는 프런트엔드 개발자들에게 실용적인 컴포넌트 또는 자주 작성되는 페이지 컨셉 토대로 단일 페이지의 작은 토이 프로젝트를 챌린지로서 제안한다.
처음으로 가볍게 진행해볼 챌린지가 무엇이 있을까 고민하다가 Accordion 챌린지 를 발견했다.
웹 페이지나 앱에서 많은 양의 콘텐츠를 작은 공간에 효율적으로 보여주기 위한 UI 컴포넌트이다.
해당 UI 컴포넌트는 읽는 것이 중요하지 않는 부분에 대해서 페이지와 스크롤을 줄이는 목적으로 주로 사용된다고 한다.
다음은 본인이 작성한 Accordion.tsx 컴포넌트의 전체 코드이다.
간단한 미니 프로젝트이기 때문에 css-in-js, tailwind 또는 react 에서 자주사용되는 유틸 라이브러리 등은 사용하지 않았다.
const Accordion: React.FC<Props> = ({ title, textContent, className }) => {
const [isShowTextContent, setIsShowTextContent] = useState<boolean>(false);
// 사용자의 상호작용에 의해서 visible 를 제어하는 컴포넌트의 ref
const contentSectionRef = useRef<HTMLDivElement>(null);
const onClickButton = () => {
setIsShowTextContent(bool => !bool);
/**
* 코드 생략 됨. 추가적으로 본문 내에서 설명하겠습니다.
**/
};
const adjustedTitleFontCSS: CSSProperties = {
fontWeight: isShowTextContent ? 'bold' : 'normal',
};
return (
<div className={`accordion base-font ${className}`}>
<section className="accordionTitleSection">
<span style={adjustedTitleFontCSS}>{title}</span>
<button
type="button"
onClick={onClickButton}
className={isShowTextContent ? 'button-rotate' : ''}
>
<PlusSvg />
</button>
</section>
<section className="accordionContentSection" ref={contentSectionRef}>
<span>{textContent}</span>
</section>
</div>
);
};
해당 컴포넌트는 isShowTextContent 라는 React state 하나를 두고 사용자의 클릭 이벤트 발생 여부에 따라 해당 state 를 갱신하면서 UI 구조를 변경한다.
컴포넌트의 구조가 간단하다보니 자세한 설명은 생략하고 onClickButton 에 대한 설명과 함께 새롭게 배운 몇 가지 프런트엔드 기술을 소개하려고 한다.
onClickButton 내부 로직에 대해서 현재 코드 상으로 보면 혼동의 여지가 있어 제거하였다.
시각적인 상호적용이 없는 버튼은 심심하다.
open, close 여부에 따라 45도 회전 애니메이션(rotate)이 발생하는 버튼을 구현해보자.
일단 상단 코드 내에서 보면 버튼에 대한 상호작용 코드는 다음과 같다.
// 버튼을 클릭하면 상태를 변경한다.
const onClickButton = () => {
setIsShowTextContent(bool => !bool);
// ...
};
...
<button
onClick={onClickButton}
// 상태의 bool 여부에 따라 className 을 업데이트한다.
className={isShowTextContent ? 'button-rotate' : ''}
>
...
button rotate 구현을 위한 js 작업은 끝났고 이제 css 를 보자
button[type="button"] {
...
// transform 요소에 대해 해당 transition 을 적용한다.
transition: transform 0.25s ease;
}
.button-rotate {
transform: rotate(45deg);
}
js, css 를 다음과 같이 작성해두면 실제로 button element 의 classList 에 isShowTextContent 의 상태 여부에 따라 .button-rotate 클래스가 추가/제거 되면서 자연스러운 UI 상호작용이 구현된다.
Accordion UI 컴포넌트의 핵심이다.
이제 onClickButton 함수에 대한 전체 부분을 공개하겠다.
const onClickButton = () => {
setIsShowTextContent(bool => !bool);
const maxHeightReconciler = new ReconcileMaxHeightElement(contentSectionRef);
maxHeightReconciler.reconcile();
};
ReconcileMaxHeightElement 은 본인이 직접 작성한 클래스이며, 왜 이런식으로 코드를 작성하였는 지 우선 설명하려고 한다.
처음 작성되었을 때 코드(ReconcileMaxHeightElement 가 없는 코드)
const onClickButton = () => {
setIsShowTextContent(bool => !bool);
if (contentSectionRef.current) {
const { current: element } = contentSectionRef;
const { maxHeight } = element.style;
if (maxHeight === '' || maxHeight === '0' || maxHeight === '0px') {
element.style.maxHeight = element.scrollHeight + 'px';
} else {
element.style.maxHeight = '0';
}
}
};
본인은 해당 코드를 작성하고나서 다음과 같은 의문이 들었다.
다른 React 개발자가 해당 컴포넌트의 전체 코드를 유지보수를 위해 관찰한다고 할 때
jsx 구문 내부에 ref 로 참조되고 있기 때문에 결국 값이 존재할 것을 알고있는데 해당 코드에서 falsy 값 체크를 진행하고 있을 필요가 있을까 싶었다.
또한 개인적으로 UI 컴포넌트 내부에는 가능한 View 를 보여주는 코드만을 두고 싶었다.
그래서 다음 클래스를 작성하여 falsy check 로직과 현재 element 의 maxHeight 값에 따른 maxHeight 속성 값 조정 역할을 인가하였다.
export default class ReconcileMaxHeightElement {
private readonly element: HTMLElement;
public constructor(element: React.RefObject<HTMLElement | null>) {
if (element.current === null) throw Error("element RefObject is null");
const { current } = element;
this.element = current;
}
public getElement(): HTMLElement {
return this.element;
}
// 핵심 부분
public reconcile(): void {
const { maxHeight } = this.element.style;
if (maxHeight === '' || maxHeight === '0' || maxHeight === '0px') {
this.element.style.maxHeight = this.element.scrollHeight + 'px';
} else {
this.element.style.maxHeight = '0';
}
}
}
해당 클래스를 활용한 코드는 상단 내용과 같이 다음과 같다.
const onClickButton = () => {
setIsShowTextContent(bool => !bool);
const maxHeightReconciler = new ReconcileMaxHeightElement(contentSectionRef);
maxHeightReconciler.reconcile();
};
개인적으로 훨씬 깔끔한 것 같다.
ReconcileMaxHeightElement 에 대해 작성해두고 드는 생각은
1. 해당 UI 컴포넌트에서 관리하는 state 값 기반으로 동작하는 것이 아님 ( 현재 값 기반 maxHeight 값 조정 )
2. reconcile 로 이름지은게 적절했는 지
정도 문제점 생각이 드는 것 같다.
이런 간단한 컴포넌트를 설계하고 작성하면서도 "여기 부분에 대해서는 이렇게 하는게 적절하겠다!" 싶은 생각보단 여러 의문이 든다.
더 많이 프런트엔드 코드를 작성하고 잘하는 분들의 설계를 엿보며 확고한 주관을 갖추는게 좋을 것 같다.
본인이 진행한 프로젝트에 대한 화면 및 전체 코드는 하단 링크에 전부하였다.
글 내용 유익하셨다면 좋아요 부탁드립니다! 행복한 하루 되세요😊