1. 들어가며…
- 얼마 전에 컨텐츠를 접었다 펼칠 수 있는 UI가 필요해, 아코디언(accordion) 컴포넌트를 만들었다. 이때 라이브러리의 아코디언을 참고했는데, 공통으로 제공하는 기능이지만 구현 방식이 조금씩 다른 걸 볼 수 있었다.
- 그래서 이번 시간에는 구현 시 참고했던 라이브러리들의 구조를 조금 더 살펴보는 시간을 가져보겠다.
2. 라이브러리별 아코디언 구조 맛보기
📌 라이브러리의 아코디언은 어떤 구조를 가질까?
여러 UI 라이브러리 중 MaterialUI, NextUI, Vuetify의 아코디언 구조를 확인해보았다.
1) 아코디언이 뭘까?
- 아코디언은 사용자가 필요한 정보를 보거나 숨길 수 있는 UI이다.
- 주로 Q&A나 FAQ와 같이 많은 정보를 효율적으로 표시할 필요가 있는 곳에서 사용된다.
- 아코디언을 클릭하면 확장되어 내용이 표시되고, 다른 섹션을 클릭하여 기존 섹션을 접을 수도 있다.
- 아래 예시는 애플의 베타 사이트의 자주 묻는 질문 화면이다.
2) 어떤 구조를 가질까?
(1) MaterialUI의 Accordion
- MaterialUI는 리액트 기반 UI 라이브러리로, 90k 이상의 star를 받은 유명한 라이브러리이다.
- 아코디온 관련 컴포넌트는 크게 4가지로 구성되어 있다.
컴포넌트 | 설명 |
---|---|
Accordion | 관련 컴포넌트를 그룹화하기 위한 래퍼 |
AccordionSummary | Accordion 헤더를 위한 래퍼로, 클릭할 때 내용을 확장하거나 접음 |
AccordionDetails | Accordion 내용을 위한 래퍼 |
AccordionActions | 여러 버튼을 그룹화하는 옵셔널 래퍼 |
- 기본 형태는 아래와 같다.
<Accordion>
<AccordionSummary>
제목 영역
</AccordionSummary>
<AccordionDetails>
내부 내용
</AccordionDetails>
</Accordion>
- 그럼, 아코디온 코드 내부를 어떻게 구성되어 있을까?
- 아래 이미지는 아코디언 관련 컴포넌트 중
Accordion
코드 내부이다.
번호 | 설명 |
---|---|
(A) | {… other} 로 외부에서 여러 속성을 받는다. |
(B) | ContextAPI의 Provider 로 데이터를 하위 컴포넌트로 전송한다. |
(C) | { children } 으로 외부에서 컴포넌트를 주입할 수 있으며,TransitionSlot으로 { chidren } 에 애니메이션을 적용된다. |
- 아코디온 제목 영역의 컴포넌트는 어떻게 구성되어 있을까?
AccordionSummary
를 살펴보자
번호 | 설명 |
---|---|
(A) | forwardRef 는 컴포넌트에 ref 를 전달하는 함수다.이 함수로 부모 컴포넌트가 자식 컴포넌트의 DOM 노드에 직접 접근할 수 있다. |
(B) | WAI-ARIA 속성(aria-expanded, aria-controls)을 사용해 아코디언 컴포넌트의 접근성을 향상시켰다.이 속성은 스크린 리더 사용자가 UI를 더 쉽게 탐색하고 이해할 수 있도록 도울 수 있다. |
(C) | { children } 으로 외부에서 컴포넌트를 주입할 수 있다. |
(D) | 만약 외부에서 expandIcon 을 지정했다면, 조건부로 expandIcon 을 렌더링한다. |
📌 [요약] materialUI의 Accordion은 ContextAPI의
Provider
로 데이터를 하위 컴포넌트에 전송하고,{ chidren }
으로 외부 컴포넌트를 주입한다. 스크린 리더 사용자를 위해 WAI-ARIA 속성을 지정했다.
컴포넌트의 경우, 역할에 맞게 총 4개의 컴포넌트로 선언했다.
(2) NextUI의 Accordion
- NextUI는 리액트 기반 UI 라이브러리이며, 2023년에 출시했다.
- TailwindCSS 기반으로 구현됐으며, TailwindCSS와 다른 점은 컴포넌트 단위로 기능 확장이 가능하다는 점이다.
- 아코디온 관련 컴포넌트는 크게 2가지로 구성되어 있다.
컴포넌트 | 설명 |
---|---|
Accordion | 아코디언 항목 목록을 표시하는 주요 컴포넌트 |
AccordionItem | 단일 아코디언 항목을 표시하는 데 사용되는 아이템 컴포넌트 |
- 기본 형태는 아래와 같다.
<Accordion>
<AccordionItem key="1" aria-label="Accordion 1" title="제목 영역">
내용 영역
</AccordionItem>
</Accordion>
- 아래 이미지는
Accordion
컴포넌트의 내부 코드이다. 렌더링 영역의 코드를 살펴보았다.
번호 | 설명 |
---|---|
(A) | useMemo 훅으로 배열 내부의 값이 변할 때, content 를 재계산한다. |
(B) | 반복문으로 state.collection 을 순회해 < AccordionItem /> 컴포넌트를 렌더링한다. |
(C) | isSplitted 값에 따라 < Divider /> 로 아이템 간의 구분선을 추가한다. |
(D) | disableAnimation prop에 따라 content 를 직접 렌더링하거나 < LayoutGroup /> 컴포넌트 내부에서 렌더링하여 애니메이션을 적용한다. |
AccordionItem
는 아래와 같은 구조를 가지고 있다.
번호 | 설명 |
---|---|
(A) | 헤더 영역을, button 태그로 선언했다. |
(B) | startContent 가 있는 경우, startContent 을 보여준다. |
(C) | 헤더 영역의 내용을 렌더링하는 코드로, title , subTitle 값이 있다면 각각 렌더링한다. |
(D) | useMemo 훅으로 배열 내부의 값(ex. isOpen, children)이 변할 때, 내용 영역을 재계산한다.{ children } 으로 외부 컴포넌트 주입을 가능토록 한다. |
📌 [요약] NextUI는
useMemo
로 컴포넌트 렌더링을 최적화했다. 헤더 영역은 자유도에 제한이 있으나, 내용 영역은{ chidren }
으로 외부 컴포넌트 주입을 가능토록 했다.
(3) Vuetify의 Expansion panel
- Vuetify는 뷰 기반 UI 라이브러리로, 38k 이상의 star를 받은 유명한 라이브러리이다.
- vuetify은 아코디온 형태의 UI를
Expansion panels
(확장 패널)로 정의했다. - 확장 패널 관련 컴포넌트는 크게 4가지로 구성되어 있다.
컴포넌트 | 설명 | 슬롯 |
---|---|---|
v-expansion-panels | 기본 컴포넌트 | - |
v-expansion-panel | v-expansion-panel-text 와 v-expansion-panel-title 을 포함하는 하위 컴포넌트 |
- |
v-expansion-panel-title | 확장 패널의 제목을 표시하는 데 사용되는 하위 컴포넌트 | #title |
v-expansion-panel-text | 확장 패널의 텍스트를 표시하는 데 사용하는 하위 컴포넌트 | #text |
- 기본 형태는 아래와 같다.
<v-expansion-panels>
<v-expansion-panel title="제목 영역" text="내용 영역" />
</v-expansion-panels>
- 아래 이미지는
ExpansionPanel
컴포넌트의 내부 코드이다. 렌더링 영역의 코드를 살펴보았다.
번호 | 설명 |
---|---|
(A) | 컴포넌트 상태에 따라, 클래스나 스타일을 조건부로 적용하며, 클래스 형식은 BEM 방식을 쓴다. |
(B) | expansion-panel 에 그림자 효과를 추가하기 위한 요소이다. |
(C) | title props 나 title slot 을 지정한 경우 패널 제목 영역을 렌더링하며, 레이아웃은 < ExpansionPanelTitle /> 을 사용한다. |
(D) | text props 나 text slot 을 지정한 경우, 패널 내용 영역을 렌더링하며, 레이아웃은 < ExpansionPanelText /> 을 사용한다. |
(E) | 사용자가 expansion-panel 내부에 추가적인 내용을 삽입할 수 있도록 하는 기본 슬롯이다. |
📌 [요약] vuetify은 아코디온 형태의 UI를 Expansion panels로 정의했다.
material-ui처럼 컴포넌트를 역할에 따라 4가지 컴포넌트로 구성했다.
클래스명의 경우 독립성과 커스터마이징을 위해 BEM을 사용했으며, slot과 props로 내용을 수정할 수 있다.
3) 어떤 기능을 제공할까?
- 각 라이브러리 별로 제공하는 기능 중, 핵심 기능을 제공하는지 여부를 체크해보았다.
용도 | MaterialUI | NextUI | Vuetify |
---|---|---|---|
비활성화 처리가 가능할까 | disabled 속성 제공 | isDisabled 속성 제공 | disabled 속성 제공 |
펼치기를 기본 설정할 수 있을까 | defaultExpanded 속성 제공 | defaultSelectedKeys 속성 제공 | v-model로 가능 |
펼치기, 접기를 컨트롤할 수 있을까 | expanded 속성 제공 | selectedKeys 속성 제공 | v-model로 가능 |
아이콘 변경이 가능할까 | expandIcon 속성 제공 | indicator 속성 제공 | expand-icon 속성 제공 |
열기, 닫기 동작을 감지하는 이벤트가 있을까 | onChange 이벤트 제공 | onSelectionChange이벤트 제공 | v-model로 감지 가능 |
콘텐츠 영역을 동적으로 변경할 수 있을까 | children으로 가능 | children으로 가능 | slot이나 props로 가능 |
애니메이션 변경이 될까 | props로 가능 | MotionProps로 가능 | v-expand-transition API 제공 |
아이템 그룹에서 한 아이템만 열리도록 할 수 있을까 | 가능 | selectionMode 속성 제공 | 가능 |
스타일 변경이 될까 | 가능 | 가능 | 가능 |
여러 스타일 타입을 제공할까 | X | variant 속성 제공 | variant 속성 제공 |
4) 왜 details 태그를 쓰지 않았을까?
- materialUI는
div
, nextUI는button
+div
, Vuetify는button
+div
로 아코디언을 구현했다. - 어? 그런데
<details>
를 사용하면, 쉽게 아코디언을 구현할 수 있고 심지어 내부 콘텐츠 검색도 가능한데..
See the Pen detail tag default by KumJungMin (@kumjungmin) on CodePen.
💡 왜 라이브러리는 아코디언을 구현할 때
<details>
대신<div>
,<button>
을 사용했을까?
(1) 일부 브라우저 호환 문제가 있다.
- can i use 사이트로
<details />
의 브라우저 호환성, 일부 문제를 확인했다. - 브라우저 호환성의 경우, Firefox, Opera 이전 버전에서
<details />
를 제공하지 않고 있었다. - 또한,
<details />
는 일부 브라우저에서 기본 폰트가 작아보이는 현상, toggle 이벤트가 작동하지 않는 문제가 있다.
(2) 스타일 커스텀이 용이하지 않다.
- 두 번째, 스타일 커스텀이
<div>
나<button>
보다 자유도가 낮다. <div>
나<button>
에서는 아이콘의 위치나 스타일을 쉽게 변형할 수 있다.- 하지만
<details>
의 경우,list-style
,list-style-type
,list-style-image
로 스타일을 변경해야한다.
See the Pen custom details tag by KumJungMin (@kumjungmin) on CodePen.
5) 만약 Accordion 컴포넌트를 만든다면?
📌 만약 Accordion 컴포넌트를 만든다면, 라이브러리를 참고하여 아래 리스트 적용을 고려해볼 수 있다!
(단, 요구사항 이상의 구현은 오버엔지니어링이 될 수 있기에 확장성만 열어둬도 됨)
- 외부에서 스타일링이 되도록
BEM
방식으로 클래스 선언하기
vg-accordion--active
vg-accordion--disabled
vg-accordion__shadow
...
- 여러 기능을 제공한다면
Accordion
컴포넌트 하나만 제공하는 게 아닌, 역할에 따라 컴포넌트 분리하기
|- Accordion
|-- AccordionSummary
|-- AccordionDetails
...
- 제목, 내용 영역의 정보는 텍스트를 넘기는 방식과 컴포넌트를 주입하는 방식 2가지 제공하기
<!-- 텍스트 넘기는 방식 -->
<AccordionSummary text="제목 영역" />
<!-- 컴포넌트 주입 방식 -->
<AccordionSummary >
<div>제목 영역</div>
<AccordionSummary />
- 컴포넌트 주입의 경우 리액트는
{ children }
를, 뷰는slot
으로 구현하기 - 리액트는
useMemo
로 UI 컴포넌트가 특정 값이 바뀔 때만 렌더링하여 최적화하기 - 외부에서 열기, 닫기를 컨트롤할 수 있어야 하며, 기본값 설정도 가능하도록 제공하기
- 컨텐츠 내부 검색이 되어야 한다면,
hidden=until-found
적용하기 - 아코디온을 열거나 닫을 때, 이를 감지할 수 있는 이벤트 제공하기
onChange event
v-model // vue의 경우 v-model로 가능
- 아이콘을 컴포넌트로 변경할 수 있게 하기
<AccordionSummary expand-Icon={<ArrowIcon />} />
- 아코디언 그룹에서 하나의 아코디온만 열리도록 하거나 동시에 열릴 수 있게 인터페이스 제공하기
- 여러 디자인이나 애니메이션 타입 제공하기(ex.
variants
) - 비활성화 제공하고, 비활성화 기본 스타일 제공하기 (ex.
disabled
) - 접근성을 위해 aria- 속성을 기본값으로 지정하기
댓글