Comment
댓글 목록을 렌더링하고 페이지네이션·답글 버튼·스크롤 위치 복원 기능을 제공하는 컨테이너 컴포넌트
개요
Comment 컴포넌트는 작성자·작성일·답글 대상자·본문·답글 버튼으로 구성된 댓글 항목을 스크롤 가능한 목록으로 제공합니다. 헤더는 작성자, 작성일, 답글 대상자 순서로 표시합니다.
주요 특징
- ✅ 댓글 목록 렌더링: 작성자·작성일·답글 대상자·본문 구조
- ✅ 요소로 넘기기: 행 필드에 문자열 대신 링크·강조 등 요소 전달
- ✅ renderItem과 부품: 프로젝트 고유 행 타입과 다른 행 모양
- ✅ 더보기 페이지네이션: 버튼 클릭으로 추가 댓글 로드
- ✅ 로딩 상태: 스피너 오버레이 표시
- ✅ 빈 상태: 안내 문구 표시
- ✅ 답글 버튼: 클릭 시 댓글 데이터 콜백 전달
- ✅ 스크롤 위치 관리: initialScrollTop + ref.getScrollTop()
기본 사용
댓글 목록을 렌더링합니다.
Preview
요소로 넘기기
author, date, replyTo, content는 문자열 대신 요소를 받습니다. 링크, 멘션, 강조를 넣을 때 씁니다. 본문의 줄바꿈 유지는 요소에서도 같습니다.
Preview
renderItem과 부품
기본 행은 프리셋으로 쓰고, 모양이 다르면 renderItem을 씁니다. renderItem은 행을 바꿀 때, 부품은 그 행 안을 조립할 때 씁니다.
프로젝트 고유 행 타입은 Comment<T>로 넘기고 getItemId로 키를 정합니다. 스크롤·빈 상태·로딩·더보기 틀은 그대로 Comment가 그립니다. 아래 예제는 내 댓글에는 삭제 버튼, 다른 사람 댓글에는 색을 지정한 답글 버튼을 둡니다. Button의 색 prop(backgroundColor, color)은 variant="default"에서만 적용됩니다.
Preview
빈 상태
댓글이 없을 때 안내 문구를 표시합니다.
Preview
로딩 상태
데이터를 불러오는 동안 스피너 오버레이를 표시합니다.
Preview
더보기 (페이지네이션)
hasMore=true이면 더보기 버튼이 표시되고, 클릭 시 onLoadMore가 호출됩니다.
Preview
표시 중: 2 / 5
답글 버튼 클릭
답글 버튼 클릭 시 onReply 콜백으로 해당 댓글의 index와 데이터가 전달됩니다.
Preview
스크롤 위치 관리
내부 스크롤 영역은 Foundation ScrollArea / ScrollBar를 사용합니다. 기존 height, initialScrollTop, ref.getScrollTop(), onLoadMore API는 그대로이며 실제 Viewport 위치를 조회·복원합니다. 더보기는 기존처럼 버튼 클릭으로 동작합니다.
아래 예제는 초기 64px 위치를 복원하고 현재 위치를 읽습니다. 보라색 12px 스크롤바는 List의 토큰 예제와 같은 방식으로 중첩된 [data-token="scroll-area"] 루트에 --scroll-area-scrollbar-width-vertical과 --scroll-area-thumb-background를 지정한 것입니다.
scrollTop: 0px
import { useRef } from "react"
import { Comment, type CommentRef } from "@vortex/ui-cals"
function MyPage() {
const commentRef = useRef<CommentRef>(null)
// 페이지 이탈 시 저장
const saveScrollPosition = () => {
const scrollTop = commentRef.current?.getScrollTop() ?? 0
sessionStorage.setItem("commentScroll", String(scrollTop))
}
// 복원
const savedTop = Number(sessionStorage.getItem("commentScroll") || 0)
return <Comment ref={commentRef} comments={comments} initialScrollTop={savedTop} />
}API Reference
Props
Comment는 데이터, 문구, 상태, 콜백만 prop으로 받습니다. 모양과 배치 prop은 replyVariant, loadMoreIcon 밖으로 늘리지 않습니다. 답글 버튼 위치는 헤더 줄 오른쪽 고정이고, 다른 자리가 필요하면 renderItem에서 Comment.Actions를 그 자리에 둡니다.
| Prop | Type | Default | Description |
|---|---|---|---|
comments | T[] (기본 CommentItem[]) | [] | 댓글 목록 데이터. renderItem이 없으면 CommentItem 형태 |
renderItem | (item: T, index: number) => ReactNode | - | 행 렌더링. 없으면 기본 행 |
getItemId | (item: T, index: number) => string | item.id, 없으면 순번 | 행 키 |
loading | boolean | false | 로딩 상태 |
hasMore | boolean | false | 더 불러올 댓글 존재 여부 |
height | number | string | 250 | 스크롤 영역 높이 |
emptyText | string | "댓글이 없습니다." | 빈 상태 안내 문구 |
replyLabel | string | "답글" | 답글 버튼 텍스트 |
showReply | boolean | true | 답글 버튼 표시 여부 |
replyVariant | ButtonProps["variant"] | "ghost" | 답글 버튼 변형 |
replyDisabled | boolean | - | 답글 버튼 비활성 |
loadMoreLabel | string | "더보기" | 더보기 버튼 문구 |
loadMoreIcon | ReactNode | - | 더보기 버튼 문구 앞 아이콘 |
initialScrollTop | number | - | 초기 스크롤 위치. 목록이 처음 그려질 때 적용 |
className | string | - | 컨테이너 className |
style | React.CSSProperties | - | 인라인 스타일 |
Events
| Event | Type | Description |
|---|---|---|
onLoadMore | () => void | 더보기 버튼 클릭 시 호출 |
onReply | (data: CommentClickData) => void | 답글 버튼 클릭 시 호출 |
부품
모두 className, style을 받습니다. Comment 안(renderItem)에서만 씁니다.
| 부품 | 요소 | 역할 |
|---|---|---|
Comment.Item | div[role=listitem] | 행. 구분선과 여백 |
Comment.Header | div | 헤더 줄. 왼쪽 Title, 오른쪽 Actions |
Comment.Title | div | 작성자·작성일·답글 대상 묶음. 남은 폭을 쓰고 좁으면 이 안에서 줄바꿈 |
Comment.Author | span | 작성자 |
Comment.Date | time(dateTime 있을 때) / span | 작성일 |
Comment.ReplyTo | span | 답글 대상 |
Comment.Content | div | 본문. 행 폭, 줄바꿈 유지 |
Comment.Actions | div | 버튼 묶음. 놓인 자리에 서고 줄어들지 않음 |
- 위치는 조립 위치로 정합니다.
Comment.Actions를Comment.Header안에 두면 헤더 줄 오른쪽,Comment.Content뒤에 두면 본문 아래에 섭니다. - 정보가 길면
Comment.Title안에서 줄바꿈합니다. 한 줄 말줄임이 필요하면className(truncate등)으로 줍니다. - 정보 항목 사이 구분선은 Comment가 그리지 않습니다. 필요하면
renderItem에서 직접 넣습니다.
Ref (CommentRef)
| Method | Return Type | Description |
|---|---|---|
getScrollTop() | number | 현재 스크롤 위치 반환 |
Types
interface CommentItem {
id: string
author: React.ReactNode
date: React.ReactNode // 해석하지 않고 그대로 표시
replyTo?: React.ReactNode
content: React.ReactNode // 줄바꿈 유지
}
interface CommentClickData {
index: number
comment: CommentItem
}접근성
권장 사항
- ✅ 댓글 목록에
role="list"자동 적용 - ✅ 각 댓글 항목에
role="listitem"자동 적용 - ✅ 로딩 시
aria-busy="true"적용 - ✅ 로딩 중 더보기 버튼은
aria-disabled로 막고 포커스를 유지 (사라지면 스크롤 영역으로 포커스 이동) - ✅ 로딩 중 댓글 목록에
inert적용 (더보기 버튼은 목록 밖이라 영향 없음)