내 포트폴리오를 JSON 4줄로 무한히 확장 가능하게 만든 방법

발행: (2026년 2월 22일 오전 12:03 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

Sammii

새 프로젝트를 포트폴리오에 추가하는 데는 30초면 충분합니다. 컴포넌트 변경도 없고, 새로운 라우트도 없으며, 레이아웃 조정도 필요 없습니다. JSON 4줄과 이미지를 폴더에 넣기만 하면 됩니다.

전형적인 포트폴리오 유지 관리 사이클에 지쳤습니다: 멋진 무언가를 만든 뒤, 포트폴리오 사이트에 연결하는 데 한 시간을 들이고, 레이아웃을 조정하고, 새 카드가 그리드를 깨뜨리지 않는지 확인해야 했죠. 그래서 저는 처음부터 완전히 데이터‑드리븐으로 설계했습니다.

작동 방식은 다음과 같습니다.

진실의 단일 소스

내 포트폴리오의 모든 프로젝트는 하나의 파일인 projects.js에 존재합니다. 각 프로젝트는 정확히 4개의 필드를 가진 객체입니다:

{
  id: 'crystal-index',
  title: 'Crystal Index',
  techStack: 'TypeScript, Next.js, Prisma, SQL, GPT4, React 3 Fiber',
  info: 'Custom CMS for cataloguing crystals with structured filters for colour, chakra, and properties, and GPT-4-generated descriptions.',
}

그게 전부입니다. 네 줄. 전체 포트폴리오는 이 객체들의 배열에서 렌더링됩니다.

ID는 세 가지 역할을 수행한다

id 필드는 설계가 흥미로워지는 지점이다. 단순한 식별자가 아니라 동시에 세 가지 용도로 사용된다:

  1. GitHub 링크 경로 – 포트폴리오는 기본 GitHub URL에 ID를 붙여 저장소 URL을 만든다.
    crystal-indexhttps://github.com/sammii-hk/crystal-index.

  2. 이미지 파일명 조회 – 자동 생성된 이미지 맵이 ID를 올바른 이미지 파일 및 확장자로 매핑한다.
    crystal-index/assets/images/crystal-index.jpg.

  3. React 키 – 배열을 매핑할 때 ID가 고유 키 역할을 한다.

하나의 필드, 세 가지 작업. 이는 중복 데이터를 없애고 GitHub 링크, 이미지, React 키가 항상 일치하도록 보장한다.

GitHub 조직 아래의 프로젝트인 경우, ID에 조직 경로가 포함된다. 예: unicorn-poo/succulent. 이미지 유틸리티는 / 로 분리하고 마지막 세그먼트를 파일명 조회에 사용하며, 전체 경로는 올바른 GitHub URL을 만든다.

Auto‑Generated Image Map

각 프로젝트 스크린샷이 .jpg인지 .png인지 일일이 확인하고 싶지 않았습니다. 그래서 이미지 디렉터리를 스캔하고 JSON 맵을 생성하는 빌드 스크립트를 작성했습니다:

import { readdir, writeFile } from 'fs/promises';
import { join } from 'path';

const imagesDir = join(process.cwd(), 'public', 'assets', 'images');

async function generateImageMap() {
  const files = await readdir(imagesDir);
  const imageMap = {};

  files.forEach(file => {
    if (file === 'sammii.png') return;
    const name = file.replace(/\.(jpg|png)$/, '');
    const ext = file.endsWith('.png') ? 'png' : 'jpg';
    if (!imageMap[name]) imageMap[name] = ext;
  });

  const outputPath = join(process.cwd(), 'app', 'common', 'utils', 'image-map.json');
  await writeFile(outputPath, JSON.stringify(imageMap, null, 2));
}

generateImageMap();

출력은 간단한 조회표입니다:

{
  "crystal-index": "jpg",
  "lunary": "png",
  "succulent": "png",
  "day-lite": "jpg"
}

유틸리티 함수는 어떤 프로젝트 ID든 전체 이미지 경로로 변환합니다:

import imageMapData from './image-map.json';
const imageMap = imageMapData;

export const getImagePath = (projectId) => {
  const projectBaseId = projectId.split('/').pop() || projectId;
  const extension = imageMap[projectBaseId] || 'jpg';
  return `/assets/images/${projectBaseId}.${extension}`;
};

폴더에 이미지를 넣고 스크립트를 실행하면 포트폴리오가 자동으로 이를 인식합니다.

Source:

컴포넌트 변경 없음

포트폴리오에는 두 가지 완전히 다른 뷰 모드가 있습니다 — 클릭‑투‑확장 모달이 있는 반응형 그리드와 전체 화면 세로 캐러셀. 두 뷰 모두 정확히 같은 projects 배열을 사용합니다.

그리드 뷰는 배열을 순회하며 카드를 렌더링합니다:

{projects.map(project => (
   setSelectedProject(project}>
    
  
))}

캐러셀 뷰는 같은 배열을 순회하며 전체 너비 슬라이드를 렌더링합니다:

 (
    
      
    
  )}
/>

ProjectItemisGrid prop을 받아서 컴팩트 카드 레이아웃과 확장 레이아웃을 전환합니다. 같은 컴포넌트, 같은 데이터, 두 가지 프레젠테이션. 배열에 프로젝트를 추가하면 추가 작업 없이 두 뷰 모두에 자동으로 표시됩니다.

30초 워크플로우

새 프로젝트를 마치면, 나는 다음과 같이 합니다:

  1. 스크린샷을 찍는다.
  2. /public/assets/images/project-name.png 이름으로 넣는다.
  3. node scripts/generate-image-map.mjs를 실행한다.
  4. projects.js에 4줄을 추가한다.
  5. GitHub에 푸시한다.

포트폴리오가 재빌드되고 새 프로젝트가 두 뷰 모두에 나타나며, 올바른 이미지, 올바른 GitHub 링크, 올바른 레이아웃이 적용됩니다.
다섯 단계, 30초, 컴포넌트 파일은 전혀 건드리지 않는다.

Source:

그 뒤에 있는 철학

이것은 포트폴리오에서 시간을 절약하는 것만이 아니라, 어디서든 사용하는 패턴입니다.

제 점성술 앱인 Lunary는 2,000개가 넘는 기사들을 담은 마법서가 있습니다. 이들은 구조화된 데이터로 저장되고 공유 컴포넌트를 통해 렌더링됩니다. 크리스털이나 타로 카드에 대한 새로운 기사를 추가할 때 UI 코드를 건드릴 필요가 없습니다.

제 출판 도구 Spellcast는 여러 소셜 미디어 계정과 플랫폼을 관리합니다. 계정 설정은 데이터 객체로 되어 있습니다. 새로운 플랫폼을 추가한다는 것은 인터페이스를 다시 빌드하는 것이 아니라 설정에 추가하는 것입니다.

원칙은 언제나 같습니다: 데이터를 프레젠테이션과 분리합니다. 데이터 구조가 무거운 작업을 담당하게 하세요. 컴포넌트는 특정 콘텐츠에 대해 알 필요가 없을 정도로 일반적이어야 합니다.

포트폴리오에 새로운 콘텐츠를 연결하는 데 프로젝트 자체를 만드는 시간보다 더 많이 소비하고 있다면, 아키텍처가 뒤바뀐 것입니다. 포트폴리오가 여러분을 위해 일하도록 만들고, 그 반대가 되지 않게 하세요.

저는 Lunary의 창립자 Sammii이며, 실제로 자신의 출생 차트를 읽는 방법을 가르치는 점성술 앱을 만들었습니다. 저는 솔로 개발자로서 제품을 구축하는 데 있어 기술적 결정을 다룹니다. Dev.to에서 저를 팔로우하거나 GitHub에서 코드를 확인해 보세요.

0 조회
Back to Blog

관련 글

더 보기 »