8.9 KiB
8.9 KiB
AS 프로젝트 교육 자료: 고급 과정 (Advanced Course)
대상: 대규모 컴포넌트 설계와 복잡한 클라이언트 인터랙션을 처리해야 하는 시니어급 개발자
목표: FSD 아키텍처 의존성 린팅 툴(Steiger)을 이해하고, @dnd-kit 기반의 결재선 드래그 앤 드롭을 구현하며, tus-js-client를 통한 대용량 이어올리기 및 OpenTelemetry 분산 트레이싱을 연동할 수 있다.
1. Steiger & FSD 아키텍처 아키텍처 규칙 검증
Steiger는 FSD 아키텍처 규범을 실시간으로 감시하는 파일 구조 정적 분석 린터입니다.
① FSD 핵심 위반 검출 및 조치법
- 비정상 의존 관계 (Violation of Layer Hierarchy):
- 상황:
entities/user내부 컴포넌트에서features/auth-login컴포넌트를 import하려는 시도. - 해결: 하위 레이어는 상위 레이어에 대해 절대 알 수 없어야 합니다. 해당 컴포넌트의 책임을 상위 레이어(
pages혹은widgets)로 격상시키거나 하위 레이어의 성격에 맞게 분리하세요.
- 상황:
- Public API 규칙 위반 (Slicing Boundaries Bypass):
- 상황:
import { UserProfile } from "@/entities/user/ui/profile"형태로 직접 파일 경로로 접근하는 경우. - 해결:
entities/user/index.ts에서export { UserProfile } from "./ui/profile"을 선언하고, 사용하는 곳에서는import { UserProfile } from "@/entities/user"로 Public API만 접근하도록 변경해야 합니다.
- 상황:
2. @dnd-kit을 활용한 결재선 관리 화면 및 DnD 구현
결재선 관리(Approval Line Management) 화면은 드래그 앤 드롭으로 결재자 순서를 변경하며, 그리드 상에서 인라인 폼 편집을 처리하는 매우 복잡한 뷰입니다.
① 핵심 설계 요구사항
- 단일 행 편집 잠금: 신규 결재선 추가 혹은 특정 행 수정 시, 해당 행을 제외한 다른 행의 입력 및 드래그 인터랙션은 모두 비활성화되어야 합니다 (
editingRowKey상태 관리). - 드래그 정렬:
@dnd-kit을 사용하여 결재자 순서 변경을 UI에 즉시 반영한 뒤, [결재선 저장] 버튼을 통해 전체 순서를 일괄 저장합니다.
② 결재선 DnD 구현 예제 (ui/components/sortable-apln-table.tsx)
"use client";
import { useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { SortableAplnRow } from "./sortable-apln-row";
export interface IAplnMember {
aplnId: string;
atrzSeqNo: number;
atrzSeComCd: string; // 검토자, 승인자 구분
aprvrUuid: string;
aprvrNm: string;
}
export function SortableAplnTable() {
const [items, setItems] = useState<IAplnMember[]>([
{ aplnId: "1", atrzSeqNo: 1, atrzSeComCd: "RVW", aprvrUuid: "user-01", aprvrNm: "김검토" },
{ aplnId: "2", atrzSeqNo: 2, atrzSeComCd: "APR", aprvrUuid: "user-02", aprvrNm: "이승인" },
]);
// 편집 중인 행 키 (null이 아니면 드래그 및 타 행 편집 금지)
const [editingRowKey, setEditingRowKey] = useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // 클릭 클릭 중 미세한 움직임으로 드래그 활성화 방지
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event: DragEndEvent) => {
// 편집 중일 때는 드래그를 차단합니다.
if (editingRowKey) return;
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((prevItems) => {
const oldIndex = prevItems.findIndex((item) => item.aplnId === active.id);
const newIndex = prevItems.findIndex((item) => item.aplnId === over.id);
const reordered = arrayMove(prevItems, oldIndex, newIndex);
// 정렬 후 시퀀스 넘버(atrzSeqNo)를 다시 인덱스 기준으로 갱신합니다.
return reordered.map((item, index) => ({
...item,
atrzSeqNo: index + 1,
}));
});
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="w-10">정렬</th>
<th>순서</th>
<th>구분</th>
<th>결재자</th>
<th>관리</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<SortableContext
items={items.map((item) => item.aplnId)}
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableAplnRow
key={item.aplnId}
item={item}
isEditing={editingRowKey === item.aplnId}
isDisableDrag={!!editingRowKey} // 편집 모드가 켜져 있으면 전체 드래그 비활성화
onEditStart={() => setEditingRowKey(item.aplnId)}
onEditCancel={() => setEditingRowKey(null)}
onEditSave={(updated) => {
setItems((prev) =>
prev.map((p) => (p.aplnId === item.aplnId ? { ...p, ...updated } : p))
);
setEditingRowKey(null);
}}
/>
))}
</SortableContext>
</tbody>
</table>
</DndContext>
);
}
3. 대용량 파일 분할 업로드 (Tus Protocol)
일반적인 HTTP POST 업로드는 대용량 파일 전송 중 대역폭 변화나 일시적인 네트워크 끊김이 발생할 경우 처음부터 다시 업로드해야 하는 심각한 취약점이 있습니다.
AS 프로젝트는 이어올리기가 가능한 Tus Protocol 규격의 파일 업로더인 tus-js-client를 활용합니다.
import * as tus from "tus-js-client";
export function uploadFileWithTus(file: File, onProgress: (pct: number) => void) {
return new Promise<string>((resolve, reject) => {
const upload = new tus.Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API_URL}/files/upload`,
retryDelays: [0, 1000, 3000, 5000], // 연결 실패 시 재시도 간격
metadata: {
filename: file.name,
filetype: file.type,
},
onError: (error) => {
console.error("Tus upload failed:", error);
reject(error);
},
onProgress: (bytesSent, bytesTotal) => {
const percentage = Math.round((bytesSent / bytesTotal) * 100);
onProgress(percentage);
},
onSuccess: () => {
// 서버에서 부여받은 최종 파일 접근 경로 또는 고유 ID 획득
console.log("Upload complete:", upload.url);
resolve(upload.url || "");
},
});
// 업로드 개시
upload.start();
});
}
4. OpenTelemetry를 활용한 분산 트레이싱 (APM)
AS 프로젝트에는 애플리케이션의 엔드 투 엔드(End-to-End) 요청 성능 분석과 응답 속도 지연 병목 지점 감시를 위해 **OpenTelemetry(OTel)**가 연동되어 있습니다.
instrumentation.ts: Next.js 서버 사이드 API 요청 및 서버 렌더링에 대한 Span을 생성하고 익스포터로 전송합니다.instrumentation-client.ts: 브라우저 클라이언트 사이드 환경에서 API 호출(fetch,xhr) 및 주요 사용자 Interaction에 대해 브라우저 환경 성능 Span을 계측합니다.
OTel 동작 핵심 컨셉
- Context Propagation: 클라이언트에서 서버로 HTTP 요청을 보낼 때 Trace Context Header(
traceparent)를 실어 보냄으로써, 브라우저에서의 동작과 백엔드 API 서비스에서의 병목 지점 간의 연관 관계를 하나로 통합해서 관찰할 수 있게 합니다.
5. 고급 실습 과제
과제 내용
ui/components/sortable-apln-table.tsx를 확장하여, 새로운 결재선을 리스트 맨 밑에 추가하는 [행 추가] 기능을 구현하세요.- 행 추가 시 새로 생성된 행은 즉시 편집 상태(
editingRowKey = 'new-temp')여야 하며, 사용자가 저장을 누르면 REST API(POST /api/v1/elapr-apln)를 호출하고 임시 키 대신 서버에서 생성된 진짜 ID로 대체되어야 합니다. - 사용자가 입력을 다 채우지 않고 취소를 누르면, 해당 추가되었던 행이 목록에서 사라져야 합니다.
- 행 추가 시 새로 생성된 행은 즉시 편집 상태(
- 제약 조건: 모든 API 통신은 TanStack Query의 Mutation(
useMutation) 및 캐시 무효화(invalidateQueries)를 통해 완전한 동기화 흐름을 가지도록 개발하세요.