맞춤형 댓글 섹션 만들기
I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have it, I’ll translate it into Korean while preserving the original formatting and code blocks.
React 앱에 댓글 섹션 추가 – 불필요한 무게 없이
“시중에 나와 있는 모든 솔루션은 자체 UI를 강요하거나, 원하지 않는 CSS를 대량으로 제공하거나, 특정 백엔드에 묶어버립니다.”
모든 복잡한 로직을 대신 처리해 주면서도 모양과 느낌을 완전히 제어할 수 있다면 어떨까요?
바로 그것이 **@hasthiya_/headless-comments-react**의 핵심입니다.
이것은 무엇인가요?
@hasthiya_/headless-comments-react는 React용 헤드리스 댓글 엔진입니다.
hooks 집합을 제공하여 댓글 스레드 전체 상태(추가, 답글, 편집, 삭제, 반응, 정렬 등)를 단 하나의 <div>도 렌더링하지 않고 관리합니다.
UI는 여러분이 제공하고, 라이브러리는 로직을 제공합니다.
이와 같은 분리를 통해 Reddit, Discord, GitHub 혹은 다른 어떤 형태로도 댓글 섹션을 스타일링하면서도 동일한 기본 로직을 재사용할 수 있습니다.
설치
npm install @hasthiya_/headless-comments-react
# or
yarn add @hasthiya_/headless-comments-react
Core Hooks (imported from @hasthiya_/headless-comments-react/headless)
| Hook | Responsibility |
|---|---|
useCommentTree | 전체 댓글 스레드를 관리 – 댓글 목록 + CRUD + 반응 메서드. |
useComment | 단일 댓글을 관리 – 답글 상태, 편집 상태, 반응 토글, 작성자 확인. |
useSortedComments | 정렬 제공 (newest, oldest, popular). |
formatRelativeTime (from the root package) | 타임스탬프를 “5 minutes ago”와 같은 사람 친화적인 문자열로 변환. |
전체 작업 예시
아래는 최소한의 완전하게 작동하는 댓글 섹션 예시이며, 새로운 React 컴포넌트 파일(예: CommentSection.tsx)에 복사‑붙여넣기만 하면 바로 사용할 수 있습니다.
UI는 모두 여러분이 직접 구현하면 되며, 훅이 나머지 로직을 처리합니다.
/* --------------------------------------------------------------
1️⃣ Types & a mock current user
-------------------------------------------------------------- */
import type { CommentUser } from '@hasthiya_/headless-comments-react';
const currentUser: CommentUser = {
id: 'user-1',
name: 'Jane Doe',
avatarUrl: 'https://example.com/avatar.jpg',
};
/* --------------------------------------------------------------
2️⃣ Initialise the comment tree
-------------------------------------------------------------- */
import { useCommentTree } from '@hasthiya_/headless-comments-react/headless';
import { useState } from 'react';
function CommentSection() {
const tree = useCommentTree({
initialComments: [], // ← load your existing comments here
});
return ;
}
/* --------------------------------------------------------------
3️⃣ Single comment UI – uses `useComment`
-------------------------------------------------------------- */
import {
useComment,
useSortedComments,
} from '@hasthiya_/headless-comments-react/headless';
import { formatRelativeTime } from '@hasthiya_/headless-comments-react';
import type {
Comment,
UseCommentTreeReturn,
CommentUser,
} from '@hasthiya_/headless-comments-react';
function CommentItem({
comment,
tree,
currentUser,
}: {
comment: Comment;
tree: UseCommentTreeReturn;
currentUser: CommentUser;
}) {
const {
isAuthor,
edit,
reply,
reaction,
showReplies,
toggleReplies,
deleteComment,
} = useComment(comment, {
onEdit: async (id, content) => tree.editComment(id, content),
onReply: async (id, content) => tree.addReply(id, content),
onReaction: async (id, reactionId) => tree.toggleReaction(id, reactionId),
onDelete: async (id) => tree.deleteComment(id),
});
return (
{/* Header */}
**{comment.author.name}** ·{' '}
{formatRelativeTime(comment.createdAt)}
{/* Content / Edit mode */}
{edit.isEditing ? (
edit.setEditContent(e.target.value)}
rows={3}
style={{ width: '100%' }}
/>
Save
Cancel
) : (
{comment.content}
)}
{/* Action bar */}
reaction.toggle('like')}>👍
Reply
{isAuthor && (
<>
edit.startEditing(comment.content)}>Edit
Delete
)}
{/* Reply box */}
{reply.isReplying && (
reply.setReplyContent(e.target.value)}
placeholder="Write a reply..."
rows={2}
style={{ width: '100%' }}
/>
Submit
Cancel
)}
{/* Nested replies */}
{comment.replies?.length ? (
<>
{showReplies ? 'Hide' : 'Show'} {comment.replies.length}{' '}
{comment.replies.length === 1 ? 'reply' : 'replies'}
{showReplies &&
comment.replies.map((r) => (
))}
) : null}
);
}
/* --------------------------------------------------------------
4️⃣ Comment list + new‑comment input + sorting
-------------------------------------------------------------- */
function CommentList({
tree,
currentUser,
}: {
tree: UseCommentTreeReturn;
currentUser: CommentUser;
}) {
const { sortedComments, sortOrder, setSortOrder } = useSortedComments(
tree.comments,
'newest',
);
const [text, setText] = useState('');
return (
{/* ---- Sort controls ---- */}
{(['newest', 'oldest', 'popular'] as const).map((order) => (
setSortOrder(order)}
disabled={sortOrder ===
### 얻을 수 있는 것
* **완전하게 작동하는** 댓글 스레드(중첩 가능, 정렬 가능, 편집 가능, 삭제 가능, 반응 가능).
* **CSS 없이** – 모든 픽셀을 직접 결정합니다.
* **주관적인 마크업 없이** – 훅은 렌더링과 완전히 분리됩니다.
* **타입 안전** API(모든 타입이 패키지에서 내보내집니다).
## TL;DR
1. 패키지를 설치합니다.
2. `useCommentTree`를 초기화합니다.
3. 각 댓글 UI에 `useComment`를 사용합니다.
4. 선택적으로 `useSortedComments`와 `formatRelativeTime`을 추가합니다.
5. 이러한 훅 위에 원하는 UI를 구축합니다.
그것만으로도 React 앱에서 **헤드리스이며 확장 가능한 댓글 시스템**을 가질 수 있습니다. 🎉
> **Source:** ...
## iya.dev – 플랫폼 영감을 받은 스타일, 락‑인 제로
`iya.dev`는 **15개 이상의 플랫폼 영감을 받은 스타일**(Reddit, GitHub, Discord, Twitter, YouTube, …)을 보여주며, 모두 **같은 훅 호출**을 기반으로 동작합니다.
- 디자인 시스템에 락‑인되지 않음.
- Tailwind, CSS Modules, styled‑components, 혹은 일반 CSS와 함께 사용할 수 있어, 라이브러리가 전혀 방해가 되지 않습니다.
### `useComment`와 비동기 콜백
`useComment`에 전달하는 각 콜백은 **async**이며, 로컬 상태를 업데이트하기 전에 API 호출을 수행하기에 최적의 위치입니다.
```tsx
const { /* … */ } = useComment(comment, {
onEdit: async (id, content) => {
await fetch(`/api/comments/${id}`, {
method: 'PATCH',
body: JSON.stringify({ content }),
});
tree.editComment(id, content);
},
// … other handlers
});
당신의 데이터 레이어, 당신의 규칙. 라이브러리는 데이터가 어디에 저장되어 있는지, 어떻게 가져오는지에 대해 신경 쓰지 않습니다.
Features
| Feature | Details |
|---|---|
| Zero UI | 스타일이나 마크업이 강제되지 않음 |
| Full comment CRUD | 추가, 편집, 삭제, 답글 |
| Reactions | 사용자 정의 반응을 토글 |
| Sorting | 최신, 오래된, 가장 인기 있는 |
| TypeScript | 기본 제공 완전 타입 |
| Backend agnostic | 비동기 콜백에서 어떤 API든 연결 |
npm install @hasthiya_/headless-comments-react
실시간 쇼케이스와 전체 문서를 확인하여 모든 기능을 확인하세요.