반응형
https://www.youtube.com/watch?v=Av9C7xlV0fA
5:04:39
전체 데이터 흐름
| 1. 컴포넌트 렌더링 <WorkspaceSwitcher /> ↓ 2. React Query Hook 호출 useGetWorkspaces() - 캐시 확인 - 없으면 API 호출 ↓ 3. HTTP 요청 GET /api/workspaces (RPC 클라이언트 through Hono) ↓ 4. 서버 라우트 처리 route.ts - GET "/" databases.listDocuments() ↓ 5. Appwrite Database WORKSPACES_ID 컬렉션 조회 ↓ 6. 응답 반환 {data: {documents: [...], total: N}} ↓ 7. React Query 캐싱 & 상태 업데이트 - data에 저장 - 컴포넌트 리렌더링 ↓ 8. UI 렌더링 {JSON.stringify(data)} |
서버 라우트 (src/features/workspaces/server/route.ts) - 데이터 제공자
- Appwrite Database에서 모든 workspace 문서들을 조회
- listDocuments(): 특정 컬렉션의 모든 문서를 가져오는 메서드
- JSON 형태로 클라이언트에 반환
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { createWorkspaceSchema } from "../schemas";
import { sessionMiddleware } from "@/lib/session-middleware";
import { DATABASE_ID, IMAGES_BUCKET_ID, WORKSPACES_ID } from "@/config";
import { ID } from "node-appwrite";
const app = new Hono()
.get(
"/",
sessionMiddleware,
async (c) => {
const databases = c.get("databases")
const workspace = await databases.listDocuments(
DATABASE_ID,
WORKSPACES_ID,
);
return c.json({data:workspace})
}
)
.post(
...생략
)
export default app
React Query Hook(src/features/workspaces/api/use-get-workspaces.ts) - 데이터 관리자
- React Query의 역할:
- 자동 캐싱: 한 번 불러온 데이터는 메모리에 저장
- 자동 리페칭: 페이지 포커스 시 자동으로 최신 데이터 가져옴
- 로딩 상태 관리: isLoading, isFetching 자동 제공
- 에러 처리: error 객체 자동 제공
queryKey: ["workspaces"]
- queryKey의 중요성:
- React Query가 데이터를 식별하는 고유 키
- 같은 키로 여러 곳에서 호출해도 중복 요청 안 함
- 캐시 무효화할 때도 이 키 사용
import {useQuery} from "@tanstack/react-query"
import {client} from "@/lib/rpc"
export const useGetWorkspaces = () => {
const query = useQuery({
queryKey:["workspaces"],
queryFn: async () => {
const response = await client.api.workspaces.$get();
if(!response.ok){
throw new Error("Failted to fetch workspaces")
}
const {data} = await response.json();
return data;
}
})
return query
}
UI 컴포넌트(src/components/workspace-switcher.tsx) - 데이터 표시자
"use client"
import { useGetWorkspaces } from "@/features/workspaces/api/use-get-workspace"
export const WorkspaceSwitcher = () => {
// hook 호출 → {data, isLoading, error} 반환
const {data} = useGetWorkspaces();
return(
<div>
{JSON.stringify(data)} // 임시로 데이터 확인
</div>
)
}
Sidebar 통합(src/features/auth/components/sidebar.tsx) - 레이아웃 구성
<aside className='h-full bg-neutral-100 p-4 w-full'>
<Link href={"/"}>
<Image src={"logo.svg"} alt='logo' width={164} height={48}/>
</Link>
<DottedSeparator className='my-4'/>
<WorkspaceSwitcher/> {/* workspace 목록 표시 */}
<DottedSeparator className='my-4'/>
<Navigation/>
</aside>

Appwrite 이미지를 Next.js에서 표시하기
! 문제 상황
- Appwrite Storage에서 이미지 파일 ID만 받아옴 (예: 689e7d5d002ef0b5bcb2)
- 실제 이미지를 보려면 완전한 URL이 필요함
- Next.js Image 컴포넌트 사용 시 외부 호스트 설정 필요
- 401 Unauthorized 오류 발생
1. Next.js 이미지 호스트 설정(next.config.mjs)
- Next.js가 외부 이미지를 최적화하려면 remotePatterns에 도메인 등록 필수
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'fra.cloud.appwrite.io',
port: '',
pathname: '/v1/storage/buckets/**',
},
],
},
};
export default nextConfig;
2. WorkspaceAvatar 컴포넌트 구현(src/features/workspaces/component/workspace-avatar.tsx)
- mode=admin 제거함 (클라이언트에서 직접 접근하므로)
import Image from "next/image"
import {cn} from "@/lib/utils"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
APPWRITE_ENDPOINT, // 너의 appwrite 엔드포인트 (ex: fra.cloud.appwrite.io)
PROJECT_ID, // 너의 프로젝트 ID
IMAGES_BUCKET_ID // images Storage 버킷 ID
} from "@/config"
interface WorkspaceAvatarProps {
name: string,
image?: string,
className?:string
}
export const WorkspaceAvatar = ({name, image, className}:WorkspaceAvatarProps) => {
let imageUrl = '';
if(image && typeof image === 'string') {
// baseEndpoint에서 '/v1' 경로 추가
const baseEndpoint = APPWRITE_ENDPOINT.startsWith('https://')
? APPWRITE_ENDPOINT
: `https://${APPWRITE_ENDPOINT}`;
// '/v1'이 이미 포함되어 있는지 확인
const endpoint = baseEndpoint.includes('/v1')
? baseEndpoint
: `${baseEndpoint}/v1`;
imageUrl = `${endpoint}/storage/buckets/${IMAGES_BUCKET_ID}/files/${image}/view?project=${PROJECT_ID}&mode=admin`;
// https://fra.cloud.appwrite.io/v1/storage/buckets/68d8d717003253abebae/files/68da5074000291bf93ab/view?project=68ca4e5d002612399c40&mode=admin
// 만약 'mode=admin' 이 꼭 필요하다면 &mode=admin 을 붙여도 되지만 보통은 필요 없음.
return(
<div className={cn(
"size-10 relative rounded-md overflow-hidden",
className
)}>
<Image src={imageUrl} alt={name} fill className="object-cover"/>
</div>
)
}
return(
<Avatar className={cn("size-10", className)}>
<AvatarFallback className="text-white bg-blue-600 font-semibold text-lg uppercase">
{name[0]}
</AvatarFallback>
</Avatar>
)
}
3. WorkspaceSwitcher에서 사용(src/components/workspace-switcher.tsx
bunx --bun shadcn@latest add select
"use client"
import {Select, SelectTrigger, SelectValue, SelectContent, SelectItem} from '@/components/ui/select'
import { useGetWorkspaces } from "@/features/workspaces/api/use-get-workspace"
import { WorkspaceAvatar } from '@/features/workspaces/component/workspace-avatar';
export const WorkspaceSwitcher = () => {
// hook 호출 → {data, isLoading, error} 반환
const {data:workspace} = useGetWorkspaces();
return(
<div>
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="No workspace selected"/>
</SelectTrigger>
<SelectContent>
{workspace?.documents.map((item) => (
<SelectItem key={item.$id} value={item.$id}>
<div className="flex justify-start items-center gap-3 font-medium">
<WorkspaceAvatar name={item.name} image={item.imageUrl}/>
<span className="truncate">{item.name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}
Appwrite Console에서:
- Storage → 해당 버킷 선택
- Settings 탭
- Permissions에서 다음 중 하나 설정:
- Any 역할에 read 권한 추가 (공개 읽기)
- 또는 특정 사용자/팀에게만 권한 부여
권한 설정 없으면 401 Unauthorized 오류 발생!

일단.. 해결 :)
반응형
'Clone Coding' 카테고리의 다른 글
| Jira-clone - 개별 작업 공간 ID 페이지 만들기 (0) | 2025.10.02 |
|---|---|
| Jira-clone - 워크스페이스 멤버십 시스템 만들기 (0) | 2025.10.02 |
| Jira-clone - image 업로드 (0) | 2025.09.29 |
| Jira-clone - Toaster 알림 라이브러리사용 (0) | 2025.09.27 |
| Jira-clone - Appwrite로 워크스페이스 생성 기능 구현 (0) | 2025.09.26 |