
project: 이 prop의 이름이다.
: Project | null: 이 prop의 타입이다.
| (Union Type): TypeScript의 Union(합집합) 문법으로, 또는(OR)을 의미한다.
project prop은 Project 타입의 데이터가 들어오거나, null 값이 들어올 수 있다.
Project 데이터가 들어오면 page.tsx가 이 프로젝트 데이터로 모달을 열어달라고 신호를 보낸 것이다.
null 값이 들어오면 page.tsx가 모달을 닫아달라고 신호를 보낸 것이다.
onClose: 이 prop의 이름이다.
: () => void: 이 prop의 타입이 함수임을 의미하며 아무런 인자를 받지 않고 아무런 값도 반환하지 않는다는 뜻이다.
< 동작원리 >

if (!project): project 변수가 Falsy한 값인지 확인한다.
=> (Falsy한 값에는 null, undefined, false, 0, "" 등이 있다.)
return null;: 아무것도 그리지 않고 즉시 종료한다. (렌더링하지 않는다.)
< 동작원리 >
[ 모달이 닫혀있을 때 ]
[ 사용자가 카드를 클릭했을 때 ]
<div> 코드를 화면에 그린다.
fixed: position: fixed. 이 div를 페이지 스크롤과 상관없이 브라우저의 뷰포트 자체에 고정시킨다.
top-0 left-0: fixed된 div를 뷰포트의 왼쪽 위 모서리에 붙인다.
w-full h-full: div의 크기를 뷰포트의 너비와 높이만큼 꽉 채운다.
bg-black: 배경색을 검은색으로 한다.
bg-opacity-90: 배경색의 불투명도를 90%로 한다.
flex: 이 div를 flex 컨테이너로 만든다.
justify-center: 주축(가로)을 기준으로 자식('모달 카드')을 수평 중앙에 배치한다.
items-center: 교차축(세로)을 기준으로 자식('모달 카드')을 수직 중앙에 배치한다.
p-4: 이 배경 div의 안쪽에 1rem(16px)의 여백을 준다.
z-[100]: z-index: 100. Navbar가 z-50이므로, Navbar보다 더 위에 이 배경이 그려지도록 한다.
onClick={onClose}: 만약 사용자가 이 div(검은색 반투명 배경)를 클릭하면 부모로부터 props로 전달받은 onClose 함수를 실행한다.
role="dialog": 이 div는 단순한 div가 아니라, 사용자의 주의가 필요한 대화상자(팝업)라고 스크린 리더에게 역할을 알려준다.
aria-labelledby="modal-title": 이 대화상자의 제목은 id="modal-title"이라는 이름표를 가진 요소의 텍스트라고 이름표를 연결시켜 준다.
aria-modal="true": 이 div는 모달이라고 알려준다. (모달은 팝업 중에서도 배경과 상호작용할 수 없는 팝업을 뜻한다.)
aria-describedby="modal-description": 이 대화상자의 설명은 id="modal-description"이라는 이름표를 가진 요소의 텍스트라고 이름표를 연결시켜 준다.
card-bg: globals.css에 직접 커스텀한 클래스이다.
bg-[rgba(17,24,39,0.6)]: 반투명한 어두운 배경색을 직접 지정한다.
rounded-lg: 카드의 모서리를 0.5rem (8px)만큼 둥글게 깎는다.
max-w-4xl: 모달의 최대 너비를 56rem(896px)로 제한한다.
w-full: 모바일에서 이 카드의 너비를 100%로 꽉 채운다.
max-h-[90vh]: 모달의 최대 높이를 화면 높이의 90%로 제한한다.
(프로젝트 설명이 매우 길 경우, 모달이 화면 밖으로 삐져나가는 것을 방지한다.)
overflow-y-auto: 만약 내용이 max-h보다 길어지면, 세로 스크롤바를 자동으로 이 카드 안에 만든다.
relative: 이 div를 위치 기준점으로 삼는다.
p-6: (모바일) 24px의 안쪽 여백을 준다.
md:p-8: md 이상에서는 32px의 안쪽 여백을 준다.
onClick={(e) => e.stopPropagation()}: 실수로 모달이 닫히는 것을 방지한다.
=> 이 코드가 없다면?
- 이 div(모달 카드)의 부모 div(검은색 배경)에는 onClick={onClose}가 걸려있다.
- JavaScript의 이벤트 버블링 현상 때문에, 자식(모달 카드)을 클릭하면 그 클릭 이벤트가 부모(검은색 배경)에게까지 전달된다.
- 사용자가 모달 내용을 클릭해도, 부모의 onClick={onClose}가 실행되어 모달이 닫힌다.
=> e.stopPropagation()
- stopPropagation()은 이벤트 전파를 중단하라는 JavaScript의 내장 함수이다.
- 이 클릭 이벤트를 여기서 멈추고, 부모에게 전달(버블링)하지 마라는 의미이다.
<button ...>: HTML의 <button> 태그이다. 브라우저와 스크린 리더에게 시맨틱하게 알려준다.
onClick={onClose}: 이 버튼이 클릭되면 부모로부터 props로 전달받은 onClose 함수를 실행한다.
=> page.tsx는 onClose={handleCloseModal}을 전달한다.
=> handleCloseModal 함수는 selectedProject 상태를 null로 바꾼다.
=> 상태가 null이 되면 ProjectModal 컴포넌트는 return null;을 실행하여 사라진다.
absolute: 부모를 기준으로 공중에 띄운다. 모달 카드에 relative 속성이 있기 때문에 이를 기준으로 떠 있게 된다.
top-4 right-4: top: 1rem (16px), right: 1rem (16px).
=> absolute로 공중에 뜬 상태에서, 카드의 오른쪽에서 16px, 위쪽에서 16px 떨어진 위치에 이 버튼을 배치한다.
text-gray-400: (평상시) 버튼 텍스트(×)의 색상을 회색으로 설정한다.
hover:text-white: (호버 시) 텍스트의 색상을 흰색으로 변경한다.
text-4xl: × 기호의 크기를 36px로 만든다.
leading-none: line-height: 1.
=> 글자 크기가 커지면, 글자 자체의 줄 간격(line-height) 때문에 버튼이 불필요하게 두꺼워질 수 있다. leading-none은 이 줄 간격을 텍스트 크기와 똑같이 (1) 줄여서, 버튼이 딱 필요한 만큼의 높이만 갖도록 한다.
×id="modal-title": <h3> 태그에 고유한 이름표를 붙인다. (웹 접근성을 위해 사용)
=> role="dialog"와 aria-labelledby="modal-title"이 한 세트로 사용되어야만, 스크린 리더가 팝업이 떴을 때 '대화상자, 개인 포트폴리오'라고 팝업의 제목을 올바르게 읽어줄 수 있다.
text-2xl: (모바일 기본) 모바일 화면에서 제목의 글자 크기를 24px로 설정한다.
md:text-3xl: (반응형) md이상 화면에서 글자 크기를 1.875rem (30px)로 더 크게 변경한다.
font-bold: font-weight: 700. 텍스트를 굵게 만든다.
text-indigo-300: 글자 색상을 밝은 남색(indigo) 계열로 설정한다.
mb-4: 아래에 1rem (16px)의 여백을 준다.
{project.title}: ProjectModal 컴포넌트가 props로 전달받은 project 객체에서, title 속성값을 꺼내와 이 자리에 텍스트로 표시한다.
이 <div>는 <video> 태그를 감싸서 16:9 비율의 검은색 프레임을 만든다.
w-full: 이 프레임의 너비를 모달 카드 너비에 꽉 채운다.
aspect-video: aspect-ratio: 16 / 9를 의미한다.
=> 이 div의 높이를 너비에 맞춰 '16:9' 비디오 비율로 자동 설정한다.
bg-gray-900: 비디오가 로드되기 전이나, 비디오 양옆에 남는 공간을 어두운 회색으로 채운다.
rounded-md: 모서리를 6px 둥글게 깎는다.
mb-6: 아래에 1.5rem (24px)의 여백을 준다.
shadow-lg: 프레임에 부드러운 그림자 효과를 준다.
<video> : 실제 동영상을 재생하는 HTML5 <video> 태그.
key={project.id}: 사용자가 모달을 닫지 않고 다른 프로젝트를 클릭할 때, React가 인지하도록 돕는 식별자이다. (지금은 project가 바뀔 때마다 모달이 닫혔다 열리므로 key가 필수적이진 않지만, 잠재적인 버그의 원천 차단을 위해 사용했다.)
src={project.videoSrc}: props로 받은 project 객체의 videoSrc 속성값을 사용한다.
autoPlay: 비디오를 자동 재생한다.
loop: 비디오가 끝나면 무한 반복한다.
muted: 비디오의 소리를 끈다.
playsInline: 모바일에서 재생될 때, 비디오를 전체 화면으로 전환하지 말고 지금 이 프레임 안에서 재생한다.
w-full h-full: <video> 태그의 크기를 부모 div(16:9 프레임)에 꽉 채운다.
object-contain: 비디오의 가로/세로 비율을 유지하면서, 부모 div 프레임 안에 딱 맞게 들어가도록 크기를 조절한다. 비디오가 잘리지 않도록 한다.
=> 비디오의 높이가 100% 꽉 차게 되고, 원본 비율을 유지하려다 보니 너비가 w-full을 다 채우지 못하고 양옆에 남는 공간이 생길 수 있다.
rounded-md: 비디오 자체의 모서리도 둥글게 깎는다.
poster={...}: 비디오가 로드되는 동안 보여줄 대체 이미지이다.
=> placehold.co를 사용해 "Loading Video..."라는 텍스트가 적힌 임시 이미지를 보여주도록 설정했다.
비디오를 지원하지 않는 브라우저입니다.
<video> 태그를 전혀 이해하지 못하는 아주 오래된 브라우저에서만 이 텍스트가 표시된다.
id="modal-description": 이 태그에 modal-description이라는 고유한 이름표를 붙인다. (웹 접근성을 위해 사용)
=> id="modal-description"과 aria-describedby="modal-description" 두 개가 '설명'을 위한 한 세트로 사용되어야만, 스크린 리더가 팝업이 떴을 때 팝업의 설명을 올바르게 읽어줄 수 있다.
text-gray-300: 텍스트의 색상을 밝은 회색으로 설정한다.
leading-relaxed: line-height: 1.625를 의미한다.
=> 텍스트의 줄 간격을 조금 넓게 설정한다.
{project.details}: ProjectModal 컴포넌트가 props로 전달받은 project 객체에서, details 속성값을 꺼내와 이 자리에 텍스트로 표시한다.
