Communitygithub.com

Jav1es/dsh-skills

从零搭建/改造纯静态多页作品集(无框架、无构建),含数据驱动渲染、SVG 示意图、3D 与动效、性能优化、上线前验收。当用户要求"做作品集网站 / 多页站点 / 加作品卡片 / 美化页面 / 上线前检查 / 页面掉帧优化 / 部署到 GitHub Pages"时使用。

dsh-skills란 무엇인가요?

이 스킬은 개발자가 순수 정적 HTML/CSS/JavaScript를 사용하여 구축된 다중 페이지 포트폴리오 웹사이트를 생성하거나 리팩터링할 수 있도록 지원합니다. 핵심 목표는 Bootstrap 또는 Tailwind와 같은 외부 UI 프레임워크를 금지하고, 단일 JSON을 통해 데이터를 중앙 집중화함으로써 효율적인 유지보수와 확장을 가능하게 하는 것입니다. 스킬은 데이터 기반 렌더링을 구현하는 방법을 다루며, SVG 일러스트레이션 생성, SpaceX 스타일의 원근감 그리드와 같은 고성능 CSS 3D 효과(비용이 많이 드는 `backdrop-filter`는 피함), 그리고 `requestAnimationFrame`을 사용한 스크롤 기반 애니메이션을 포함합니다. JavaScript 실패로 인한 완전한 콘텐츠 손실을 방지하기 위한 점진적 개선 진입 애니메이션과 깨진 링크, `alt` 속성, 잘못된 HTML 구조를 확인하기 위한 자동화된 검증 스크립트에 특별한 주의를 기울입니다. 이 접근 방식은 GitHub Pages에 배포하는 방법을 가르치며, 배포 전에 현실적인 3G 네트워크 조건(1.6Mbps / 400ms RTT)에서의 성능 예산 추정치를 제공합니다. 실제 함정과 입증된 해결 방법(예: 내부 SVG 텍스트와 충돌하는 카드 그라데이션 및 CSS 시작 `opacity:0`의 위험성)이 특히 강조됩니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Jav1es/dsh-skills/tree/HEAD/static-portfolio-build

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

static-portfolio-build — 정적 다중 페이지 포트폴리오

기술적 경계선: HTML5 + CSS3 + 네이티브 JS만 사용. Bootstrap/Tailwind 같은 UI 프레임워크 금지, 외부 CDN 글꼴 금지. 기본 색상 ≤3개, 글꼴 ≤2개, 모든 이미지에 alt 속성 필수(SVG는 role="img" + aria-label 사용). 첫 화면 로딩 3G ≤3초(1.6Mbps / RTT 400ms 추정).

1. 아키텍처: 데이터 기반, 한 곳에서 유지보수

각 HTML에 수동으로 포트폴리오 내용을 작성하지 마세요. JSON 데이터 + 렌더링 레이어로 추출:

works-data.json    데이터 소스(스크립트 생성, 수동 수정 금지)
works-data.js      동일 데이터 인라인 버전(window.__WORKS_DATA__) — file:// 프로토콜에서 CORS 우회하여 더블 클릭 가능
works.js           렌더링 레이어: ROLES / cardHTML() / tileHTML() / bindFilter() / bindExpand()
extract-works.mjs  이전 단일 페이지에서 추출
patch-works.mjs    재번호 지정/모듈 보충/motif 할당(멱등성 필수!)

왜 JS 데이터를 인라인해야 하는가: fetch('./works-data.json')이 file:// 프로토콜에서 CORS에 차단됩니다. 사용자가 더블 클릭하면 빈 화면이 나타납니다.

멱등성 패치 스크립트: patch-works.mjs는 반복 실행 가능해야 합니다. EXTRA_TITLES를 사용하여 이미 패치된 항목을 필터링해야 합니다. 그렇지 않으면 두 번째 실행 시 "매핑 테이블에 없음" 오류가 발생합니다.

모듈 번호와 실제 디렉터리 정렬: 포트폴리오 번호를 카테고리가 아닌 로컬 디렉터리/GitHub 리포지토리의 실제 소유권에 따라 정렬합니다. "디렉터리가 있는 항목은 디렉터리 번호에 맞추고, 없는 항목은 빈자리를 채움" 방식으로 연속적이고 간격이 없도록 합니다. 번호를 변경할 때 사이트 전체 참조(진입 카드, 직무 페이지 배너, 소개 페이지 카테고리)를 동기화합니다. 진입 카드를 데이터에서 자동 생성하는 것이 가장 좋습니다. 수동 작성으로 인한 불일치를 방지합니다:

document.querySelectorAll('[data-owns]').forEach(function (el) {
  var r = WORKS.ROLES[el.getAttribute('data-owns')];
  if (r) el.textContent = '보유 ' + r.owns.length + '개 모듈(' + r.owns.join(' / ') + ')';
});

2. 스크린샷이 없을 때: 인라인 SVG 일러스트레이션

포트폴리오에는 대개 스크린샷이 없습니다. 카테고리별로 기하학적 모티프를 할당하고 인라인 SVG를 생성합니다(외부 이미지 없음, 첫 화면 이미지 요청 없음):

카테고리모티프요소
계획roadmap노드 + 점선 연결 + 마일스톤
워크플로우pipeline입력→처리→검토→출력 + 화살표 마커
에이전트neural계층형 노드 + 완전 연결 엣지
데이터metrics막대 + 꺾은선 추세
오픈소스terminal어두운 창 + 컬러 코드 줄 + 커서
산업grid청사진 그리드

각 SVG는 role="img"와 의미론적 aria-label을 포함해야 합니다.

문제 기록:

  • SVG 내부의 <text>가 카드 그라데이션에 가려짐 → 태그를 SVG 밖으로 이동, HTML 오버레이 .w-cover-tag 사용(position:absolute)
  • preserveAspectRatio는 meet 사용, slice 사용 금지(잘림 방지)
  • 배열 인덱스 오류: 모티프 데이터가 [x,y,text,color]로 작성되었지만 렌더링이 [x,y,width,color]로 해석되어 l[2]를 가져옴 → 모든 사각형이 실패하고 커서만 표시됨. 모티프를 작성할 때 인덱스 의미가 일관되는지 먼저 확인하세요.

3. 3D 및 모션 효과(보기 좋지만 프레임 드롭 없어야 함)

배경: SpaceX 스타일 원근감 그리드

.bg-grid-floor {
  position: absolute; left: -30%; right: -30%; top: 16%; height: 90%;
  background-image:
    repeating-linear-gradient(90deg,  rgba(10,10,10,.16) 0 1px, transparent 1px 74px),
    repeating-linear-gradient(180deg, rgba(10,10,10,.15) 0 1px, transparent 1px 40px);
  transform: perspective(520px) rotateX(72deg) translateZ(0);
  transform-origin: 50% 0%;
  mask-image: linear-gradient(to bottom, transparent 0%, #000 11%, #000 52%, transparent 90%);
  animation: floorDrift 30s linear infinite;   /* background-position 변경, transform 사용 금지 */
}

그 위에 수평선 광선 + 세 개의 떠다니는 광점(각각 24/29/34초 호흡)을 겹칩니다.

성능 철칙(프레임 드롭의 분기점)

절대 filter: blur()와 backdrop-filter를 사용하지 마세요. 이 두 가지는 매 프레임마다 뒤 내용을 다시 래스터화/샘플링하여 스크롤 프레임 드롭의 주요 원인입니다:

  • 부드러운 광점: radial-gradient(circle, rgba(...).10, transparent 68%) 사용 — 그라데이션 자체가 부드럽기 때문에 blur 불필요
  • 모서리 유리 네비게이션: 거의 불투명한 배경 rgba(255,255,255,.96) 사용
  • 섹션 바닥: 반투명 흰색 배경 사용, backdrop-filter 사용 금지

애니메이션은 transform / opacity만 조작(합성기 처리, 레이아웃/페인트 트리거 없음). 스크롤 패럴랙스는 requestAnimationFrame 스로틀링 + translate3d 사용.

카드 3D 기울이기

pointermove는 CSS 변수를 작성하고 DOM을 건드리지 않음:

.tilt { transform: perspective(1000px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)); }
.tilt.is-tilting { transition: transform .06s linear; }  /* 따라갈 때 transition 감소 */

.tilt .w-body { transform: translateZ(18px); }로 패럴랙스 부양 효과 생성; ::before 방사형 하이라이트가 --mx/--my를 따름.

hover 변위와 진입 애니메이션이 transform을抢夺함 → hover는 box-shadow/border-color만 변경, 변위는 모두 .tilt에 위임.

4. 진입 애니메이션: 절대 순수 CSS opacity:0 사용 금지

이것이 가장 큰 함정입니다. 순수 CSS로 시작 시 숨기고 JS로 .is-revealed를 추가하는 방식은, IntersectionObserver가 콜백하지 않으면(헤드리스 브라우저, JS 오류, 비정상 경로) 전체 페이지가 영구적으로 보이지 않게 됩니다.

올바른 방법: JS가 필요에 따라 숨김 상태 클래스를 추가하고, 클래스가 없는 요소는 기본적으로 보이게 합니다:

.w-card.reveal-armed { opacity: 0; transform: translateY(18px); }
.is-revealed { opacity: 1 !important; transform: none !important; }
list.forEach(function (el) {
  var r = el.getBoundingClientRect();
  var inView = r.top < innerHeight + 150 && r.bottom > -150;
  if (!inView) el.classList.add('reveal-armed');   // 뷰포트 밖의 요소만 숨김
  io.observe(el);
});
// 폴백: 3초 후에도 표시되지 않으면 강제 표시
setTimeout(function () { /* force reveal */ }, 3000);

뷰포트 내의 요소는 즉시 보이고, 뷰포트 밖의 요소만 무장됨 — 첫 화면이 애니메이션에 의해 지연되지 않고, 비정상 경로에서도 빈 화면이 발생하지 않습니다.

5. 스크립트 주입: <script> 열기/닫기 쌍 검증

여러 페이지에 코드를 일괄 주입할 때 <script>와 </script>의 개수가 동일한지 반드시 검증해야 합니다.

실제 사고: 주입 시 여는 태그 <script>를 하나 더 작성 → 이후의 인라인 스크립트 블록이 JS 파서에 의해 모두 소멸 → 네 개의 직무 페이지 렌더링 코드가 모두 작동 중단. 페이지는 완전히 정상으로 보였습니다(제목, 내비게이션, 직무 목록 모두 있음). 포트폴리오 영역만 비어 있어 육안으로 거의 발견할 수 없었습니다.

정규식 주입 시 주의: LastIndexOf('<script>')는 <script src=...>도 일치하여 정의가 태그 밖에 삽입되어 표시 텍스트가 됩니다. 명확한 앵커 문자열을 사용하세요.

6. 검증 방법(재사용 가능)

스크린샷 검증(중요: 동일 파일 이름은 캐시 사용)

vision_html_screenshot은 동일 파일 이름에 대해 캐시를 사용합니다. CSS를 변경한 후에는 반드시 Copy-Item을 사용하여 .verify/ 디렉터리에 임시 이름으로 복사한 다음 스크린샷을 찍고, 검증 후 샌드박스를 삭제하세요.

동기 프로브(load + setTimeout 사용 금지)

헤드리스 환경에서 window.load + setTimeout은 신뢰할 수 없습니다. 프로브를 렌더링 콜백 내부에 작성(동기 실행)하고 position:fixed;bottom:0인 <pre>에 출력하세요:

WORKS.loadData(function (data) {
  root.innerHTML = WORKS.listByRole(data.items, 'fde');
  var d = document.createElement('pre');
  d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#000;color:#0f0;font:11px monospace;padding:4px;margin:0';
  d.textContent = 'CARDS=' + root.querySelectorAll('.w-card').length;
  document.body.appendChild(d);
});

⚠️ 프로브에 top 위치 지정을 사용하지 마세요 — 카드 3D 기울기의 transform이 fixed 요소를 밀어낼 수 있습니다.

일괄 검증(페이지별 파일 읽기보다 효율적)

  • 깨진 링크: 정규식으로 ./xx/yy.md 추출 → Test-Path로 각각 검증
  • 번호 연속성: Compare-Object로 예상 시퀀스 대비 실제 시퀀스 비교
  • 디렉터리 정렬: PowerShell에서 '01' 비교 시 int 1로 변환되므로 [string] 강제 형변환 필수
  • 고가 기능 잔여: [regex]::Matches($css,'filter:\s*blur|backdrop-filter'), 주석 구분에 주의

7. GitHub Pages에 배포

gh repo create <user>/<repo> --public --description "..."
git remote add origin https://github.com/<user>/<repo>.git
git push -u origin main

배포 전 필수 확인:

  1. git ls-files로 종속 디렉터리가 실제로 추적되는지 확인(로컬에 파일이 있다고 git이 추적하는 것은 아님)
  2. 원격에 이미 콘텐츠가 있다면 먼저 git ls-tree --name-only origin/main으로 비교하고, 덮어써서 손실되지 않도록 주의
  3. 로컬 상대 경로에 의존하는 형제 프로젝트 디렉터리: 함께 푸시하거나 GitHub 리포지토리 주소로 변경
  4. GitHub Pages: Settings → Pages → Source에서 브랜치 선택
  5. 하위 애플리케이션(예: hitech-self-assessment/)이 있는 경우 메인 사이트 교체로 덮어쓰지 않도록 주의

성능 추정: 총 바이트 / 8 / 1.6e6 + 4 * 0.4(초)

8. 전달 전 자체 점검(web-delivery-checklist 정렬)

  • 네/다섯 페이지 모두 스크린샷 검증 완료(첫 화면만 검증한 것이 아님)
  • 깨진 링크 0개, 누락된 alt 0개
  • <script> 열기/닫기 쌍 정상, CSS 중괄호 균형
  • filter: blur / backdrop-filter 실제 적용 수 = 0
  • 3G 추정 < 3초
  • 기본 색상 ≤3개, 글꼴 ≤2개, 외부 CDN 리소스 0개
  • 모바일(390px) 햄버거 메뉴 펼침/접힘 가능, aria-expanded 동기화
  • prefers-reduced-motion에서 모든 애니메이션 비활성화

관련 스킬