[Portfolio] ProjectCard.tsx구현

yujin·2025년 11월 12일

프로젝트

목록 보기
15/26
post-thumbnail

⭐ ProjectCard.tsx 코드 구현 및 의미

1. interface

  • ProjectCard 컴포넌트를 사용하려면 이 2가지 항목을 반드시 약속된 타입으로 전달해야 한다고 알려주는 규칙
interface ProjectCardProps {
  project: Project;  // 조건 1: 'project'라는 항목이 반드시 있어야 한다.
  onClick: () => void;  // 조건 2: 'onClick'이라는 항목이 반드시 있어야 한다.
}

조건 1

project: Project;

  • project: ProjectCard 컴포넌트가 부모로부터 project라는 이름의 prop을 받아야 한다는 뜻이다.
  • : Project: Project는 이 prop의 타입이다.

=> 즉, 전달받는 project 데이터의 타입은 src/types/index.ts에서 정의한 Project 설계도(id, title, videoSrc 등이 있는)를 반드시 따라야 한다는 뜻이다.

조건 2

onClick: () => void;

  • onClick: ProjectCard 컴포넌트가 부모로부터 onClick이라는 이름의 prop을 받아야 한다는 뜻이다.
  • : () => void: 이 prop의 타입이 함수여야 하며, 이 함수는 아무런 인자도 받지 않는다. 또한 아무것도 반환하지 않고 그냥 실행되고 끝난다는 뜻이다.

필요한 이유

: 모달을 띄우는 실제 기능은 ProjectCard가 아니라, 그 부모의 부모인 page.tsx에 있는 handleProjectClick 함수가 가지고 있다.

=> 즉, ProjectCard는 사용자가 자신을 클릭했다는 사실을 부모에게 알려야 한다.
=> 이 onClick prop은 부모(page.tsx -> Projects -> ProjectCard)가 자식에게 "만약 네가 클릭되면, 내가 전달해준 이 함수를 대신 실행해 줘!"라고 말하며 넘겨주는 것이다.


2. 함수 선언

  • ProjectCard라는 이름의 React 컴포넌트를 선언한다.
  • 이 컴포넌트는 부모로부터 props 객체를 받으며, 그 안에서 project와 onClick을 바로 꺼내 쓴다.
  • 이 props 객체는 ProjectCardProps의 검사를 받아야 한다.
  • 이 컴포넌트는 '(' 뒤에 나오는 JSX를 화면에 그린다.

3. 최상위 div

  • <div>는 왼쪽 카드(정보)와 오른쪽 카드(설명)를 어떻게 배치할지 결정한다.

  • grid :<div>를 CSS Grid 컨테이너로 만든다.
    => 이 <div>의 직계 자식들(왼쪽 카드 div와 오른쪽 카드 div)을 격자판 위에 올린다.

  • grid-cols-1: (모바일 기본) 세로 1줄짜리이다.
    => 모바일 화면에서는 왼쪽 카드와 오른쪽 카드가 순서대로 수직으로 쌓인다.

  • lg:grid-cols-5: (반응형) 격자판을 5개의 세로 열로 만들라는 뜻이다.
    => 결과: 데스크톱 화면(1024px 이상)에서는 가로 5칸짜리 격자판으로 변신합니다.
    (이 5칸을 자식인 왼쪽 카드가 lg:col-span-2(2칸) 차지하고, 오른쪽 카드가 lg:col-span-3(3칸) 차지하여 2 + 3 = 5칸 레이아웃이 완성된다.)

  • gap-8: 격자판의 사이의 여백을 32px로 설정한다.
    => 모바일에서는 위아래 카드의 세로 간격, 데스크톱에서는 좌우 카드의 가로 간격이 된다.

  • items-center: 그리드 아이템(자식인 왼쪽/오른쪽 카드)들을 각자 배정된 칸 안에서 세로 방향으로 중앙에 배치한다.
    => (데스크톱에서 왼쪽 카드의 높이와 오른쪽 카드의 높이가 다르더라도, 두 카드가 서로 세로 중앙에 보기 좋게 정렬된다.)


4. 왼쪽 카드

가장 바깥 div

  • lg:col-span-2: 1024px 이상의 화면에서, 5칸 중 2칸을 차지하겠다는 뜻이다.

  • group: 자식 요소의 스타일을 부모의 hover 상태에 따라 제어하게 해준다.
    => 이 <div>에 group 클래스를 붙여놓았기 때문에, 이 <div>에 마우스를 올리면 자식 div의 group-hover:blur-sm, group-hover:scale-[1.02] 등의 클래스가 작동한다.

  • relative: 이 <div> 자체에는 아무 변화도 없지만, 자식 요소가 position: absolute를 사용할 때의 기준점이 된다.
    => 아래 코드에 있는 자식 코드인 '호버 오버레이'는 absolute inset-0 (부모의 4방향에 꽉 참) 속성을 가진다. 이 absolute는 relative가 설정된 가장 가까운 부모를 기준으로 위치를 잡는다.
    => 즉, relative는 검은색 오버레이가 이 왼쪽 카드에 정확히 겹쳐지도록 만드는 역할을 한다.

  • overflow-hidden: 이 <div>의 테두리를 벗어나는 모든 자식 요소를 숨긴다.
    => group-hover:scale-[1.02] (살짝 커짐) 효과 시 overflow-hidden이 없으면, 커진 div가 둥근 모서리 밖으로 삐져나가게 된다.

  • rounded-xl: 이 <div>의 모서리를 0.75rem (12px)만큼 둥글게 깎는다.

  • cursor-pointer: 마우스를 올리면, 커서가 기본 화살표에서 손가락 모양으로 바뀐다.

  • onClick={onClick}: 만약 사용자가 이 <div>를 클릭하면, props로 전달받은 onClick 함수를 실행시킨다.


두번째 div

  • card-bg: globals.css에 직접 커스텀한 클래스이다.

  • bg-[rgba(17,24,39,0.6)]: 반투명한 어두운 배경색을 직접 지정한다.

  • p-6: 상하좌우에 1.5rem (24px)의 안쪽 여백을 준다.

  • rounded-lg: 카드의 모서리를 0.5rem (8px)만큼 둥글게 깎는다.

  • group-hover:blur-sm: 부모 <div>에 마우스를 올리면, 이 <div>에 약한 흐림 효과를 적용한다.

  • group-hover:scale-[1.02]: 부모 <div>에 마우스를 올리면, 이 <div>를 1.02배(2%) 확대시킨다.

  • transition-all: 이 <div>의 모든 속성(흐림, 확대 등)이 변할 때 애니메이션을 적용한다.

  • duration-300: 위 애니메이션이 0.3초 동안 부드럽게 일어나도록 한다.

  • h-full: 이 카드의 높이를 부모 <div>의 높이만큼 채운다.


h3 & span

  • text-2xl: 제목 글자의 크기를 1.5rem(24px)로 설정한다.

  • font-bold: weight 값을 700으로 설정하여 제목을 굵게 만든다.

  • text-white: 글자 색상을 흰색으로 지정한다.

  • mb-2: 아래쪽에 0.5rem (8px) 여백을 준다.

  • {project.title}: 부모 컴포넌트로부터 props로 전달받은 project 객체에서, title 속성값을 꺼내와 이 자리에 텍스트로 표시한다.
    => {...}: JSX 안에서 JavaScript 변수나 표현식을 사용하겠다는 기호이다.

  • text-base: 부모의 text-2xl (24px)를 덮어쓰고, 글자 크기를 1rem (16px)로 설정한다.

  • font-medium: 부모의 font-bold(700)를 덮어쓰고, font-weight: 500으로 설정하여 덜 굵은 굵기로 만든다.

  • text-gray-400: 글자 색상을 회색으로 설정한다.

  • ml-2: margin-left: 0.5rem (8px)을 설정한다.
    => 앞의 {project.title} 텍스트와 이 <span> 텍스트 사이에 8px의 왼쪽 여백을 준다.

  • {project.isTeamProject ? "(팀 프로젝트)" : "(개인 프로젝트)"}
    => JavaScript의 삼항 연산자이다.
    => 만약 project.isTeamProject 값이 true라면 (팀 프로젝트) 텍스트를 보여준다.
    => 그렇지 않고 false라면 (개인 프로젝트) 텍스트를 보여준다."


첫번째 p

  • 이 코드는 <h3> 바로 아래에 오는 부제목 텍스트이다.

  • text-indigo-300: 텍스트의 색상을 밝은 남색(indigo) 계열로 설정한다.

  • font-semibold: 텍스트의 굵기를 세미볼드로 설정한다.

  • mb-4: 아래에 1rem (16px)의 여백을 준다.

  • {project.subtitle}: 부모 컴포넌트로부터 props로 전달받은 project 객체에서, subtitle 속성값을 꺼내와 이 자리에 텍스트로 표시한다.


두번째 p & strong

  • 이 코드는 "기간: 프로젝트 기간"을 표시한다.

  • mb-2: 아래에 0.5rem (8px)의 여백을 준다.

  • text-gray-300: 글자 색상을 밝은 회색으로 설정한다.

  • <strong> 태그: HTML 시맨틱 태그로, 텍스트가 중요한 라벨임을 의미한다.

  • font-semibold: 텍스트의 굵기를 세미볼드로 설정한다.

  • text-gray-300: 글자 색상을 밝은 회색으로 설정한다.
    => <strong> 태그가 부모(p)의 색상을 상속받지만, 굵기가 달라지면서 색상이 미세하게 달라 보일 수 있으므로, p 태그와 동일한 gray-300 색상을 명시적으로 다시 지정해준다.

  • {project.period}: 부모 컴포넌트로부터 props로 전달받은 project 객체에서, period 속성값을 꺼내와 이 자리에 텍스트로 표시합니다.


마지막 div & h4 & ul & li

  • mt-4: 위쪽에 1rem (16px)의 여백을 준다.

  • font-bold: 텍스트를 굵게 만든다.

  • text-gray-300: 글자 색상을 밝은 회색으로 설정한다.

  • mb-4: 아래쪽에 1rem (16px)의 여백을 준다.

  • <ul>: 순서가 없는 목록의 약자이다. (목록 앞에 불릿(•)이 붙는다.)

  • list-disc: 목록의 불릿 스타일을 채워진 원(•) (disc)으로 설정한다.

  • list-inside: 불릿(•)을 <li> 태그의 안쪽에 배치하여 텍스트와 함께 정렬되도록 한다.

  • text-gray-400: 이 목록 안의 모든 <li> 텍스트 색상을 회색으로 설정한다.

  • space-y-1: 자식 요소(<li>)들 사이에 세로 간격을 0.25rem (4px)씩 준다.

  • {project.role.map((role, index) => ...)}
    => {...}: JSX 안에서 JavaScript 코드를 실행합니다.
    => .map((role, index) => ...): JavaScript의 .map() 배열 함수이다.
    => 이 role 배열을 처음부터 끝까지 순회하면서, 각 항목마다 => 뒤의 코드를 실행해 줘.
    => index: 배열의 순서(0, 1, 2...)가 이 변수에 담긴다.

  • <li key={index}>{role}</li>: .map() 함수가 반환하는 결과물이다.
    => role 배열의 항목 수만큼 <li> 태그가 생성된다.
    => {role}: <li> 태그 안에 role 변수에 담긴 텍스트를 표시합니다.
    => key={index}: (React 규칙) .map()을 사용해 목록을 만들 때는, React가 각 항목을 빠르고 정확하게 구분할 수 있도록 고유한 식별자(key)를 반드시 제공해야 한다.


5. 호버 오버레이

  • 이 코드는 평소에는 투명하게 숨어있다가, '왼쪽 카드'에 마우스를 올리면 부드럽게 나타나는 검은색 반투명 상자를 만든다.

div

  • absolute: 부모를 기준으로 공중에 띄운다.

  • inset-0: top: 0; right: 0; bottom: 0; left: 0;를 한 번에 쓴 것이다.
    => 공중에 뜬 상태로, 부모의 상하좌우 4면에 꽉 차게 늘어난다.

  • bg-black: 배경색을 검은색으로 한다.

  • bg-opacity-60: 검은색 배경의 불투명도를 60%로 설정한다.

  • p-4: 안쪽에 16px의 여백을 준다. (아이콘과 텍스트가 가장자리에 붙지 않게 한다.)

  • flex: 이 div를 flex 컨테이너로 만든다.

  • flex-col: 자식 요소(아이콘, 텍스트)를 세로로 쌓는다.

  • justify-center: flex-col 상태이므로, 자식 요소들을 '세로축'의 정중앙에 배치한다.

  • items-center: flex-col 상태이므로, 자식 요소들을 '가로축'의 정중앙에 배치한다.

  • text-center: p 태그의 텍스트 자체를 가운데 정렬한다.
    => items-center가 가로 중앙 정렬인데, text-center는 왜 또 필요할까?
    => items-center는 <p> 태그 자체(상자)를 가로 중앙으로 옮긴다.
    => text-center는 그 <p> 태그 안에 있는 글자를 가운데 정렬한다.

  • opacity-0: (기본 상태) opacity: 0을 의미한다.
    => 평소에는 이 div 전체를 100% 투명하게 만들어 숨긴다.

  • group-hover:opacity-100: (호버 상태) 이 div의 opacity를 100%로 만든다.

  • transition-opacity: 오직 opacity 속성에만 애니메이션을 적용한다.

  • duration-300: opacity가 0에서 100으로 변할 때, 0.3초 동안 부드럽게 일어나도록 한다.


PlusIcon

  • className="...": PlusIcon 컴포넌트에 prop을 전달한다.

  • 여기서 전달한 className 값은 PlusIcon.tsx 파일 내부의 <svg className={className} ...> 코드에 의해, SVG 태그의 class 속성으로 그대로 적용됩니다.

  • h-10: 아이콘의 높이를 2.5rem (40px)로 설정한다.

  • w-10: 아이콘의 너비를 2.5rem (40px)로 설정한다.

  • text-white: SVG 아이콘은 텍스트처럼 취급될 수 있어서, text-white로 아이콘의 색상을 흰색으로 설정한다.

  • mb-4: 아래에 1rem (16px) 여백을 준다.



6. 오른쪽 카드 (설명)

가장 바깥 div

  • lg:col-span-3: 스크린이 1024px 이상일 때 5칸 중 3칸을 차지한다.

  • card-bg: globals.css에 직접 만든 커스텀 클래스를 적용한다.

  • bg-[rgba(17,24,39,0.6)]: bg- 접두사를 사용해 이 div의 background-color를 rgba(17, 24, 39, 0.6) (반투명한 어두운 색)로 직접 지정한다.

  • p-6: 상하좌우에 1.5rem(24px)의 안쪽 여백을 준다.

  • rounded-lg: 카드의 모서리를 0.5rem(8px)만큼 둥글게 깎는다.


첫번째 p

  • mb-4: 아래에 1rem (16px)의 여백을 준다.

  • text-gray-300: 텍스트의 색상을 밝은 회색으로 설정한다.

  • {project.description}: 부모 컴포넌트로부터 props로 전달받은 project 객체에서, description 속성값을 꺼내와 이 자리에 텍스트로 표시한다.


두번째 p

  • font-semibold: 텍스트의 굵기를 세미볼드로 설정한다.

  • text-gray-200: 텍스트의 색상을 더 밝은 회색으로 설정한다.

  • mb-2: 아래에 0.5rem (8px)의 여백을 준다.


div & span

  • flex: display: flex. 자식 요소들을 가로로 정렬한다.

  • flex-wrap: 만약 기술 스택 태그가 너무 많아서 한 줄에 다 안 들어가면, 줄바꿈을 허용하여 다음 줄로 넘어가게 한다.

  • gap-2: flex의 자식 요소들 사이에 0.5rem(98px)의 가로/세로 간격을 준다.

  • {project.techStack.map((tech) => ...)}: <span> ... </span> 태그를 배열의 항목 수만큼 생성하여 <div> 안에 넣어준다.

  • key={tech}: .map()을 사용할 때 React가 각 항목을 구분하기 위해 필요한 고유 ID이다.

  • className="...": 이 className은 2부분으로 나뉜다.

  • text-white: 태그 안의 텍스트 색상을 흰색으로 설정한다.

  • text-sm: 폰트 크기를 0.875rem (14px)로 설정한다.

  • font-medium: font-weight: 500. 텍스트의 굵기를 미디엄으로 설정한다.

  • px-3: 텍스트의 좌우에 12px의 안쪽 여백을 준다.

  • py-1: 텍스트의 위아래에 4px의 안쪽 여백을 줍니다.

  • rounded-full: 태그의 모서리를 완전한 원형(타원형)으로 깎는다.

  • ${techColorMap[tech] || "bg-gray-500/50"}:
    => techColorMap[tech]: src/data/projects.ts에서 가져온 techColorMap 객체에서, 현재 tech 변수를 Key로 사용해 Value를 찾는다.
    => || "bg-gray-500/50": ||는 OR(또는)을 의미한다. 만약 techColorMap에서 tech 이름(Key)을 못 찾으면, 기본값으로 회색(bg-gray-500/50)을 사용하라는 뜻이다.


🩷 결과

  • 화면

0개의 댓글