언제나와 같이 특별한 건 없습니다. 기존의 Skills페이지를 없애고 About 페이지에서 보여주기로 했고, 스킬을 카드 형태로 보여주는 컴포넌트와 볼 카드를 선택할 수 있는 리스트 컴포넌트를 만들었습니다.
먼저 SkillCard 컴포넌트를 간단하게 만들었습니다. 모션을 활용할 것을 고려해서, 흰색 로고만 들어있는 png 이미지와 그 뒤에 깔릴 백그라운드를 만들어주고, 타이틀 띄워주고, 설명 띄워줍니다.
interface SkillCardProps {
skill: Skill;
}
export default function SkillCard({ skill }: SkillCardProps) {
return (
<li className="flex flex-col">
<section style={{ background: `${skill.bgColorCode}` }}>
<img src={skill.imgUrl} alt={skill.title} className="w-full" />
</section>
<section>
<section>{skill.title}</section>
<ul>
{skill.description.map((item) => (
<li>{item}</li>
))}
</ul>
</section>
</li>
);
}
기술 스택 로고 svg 파일을 받아와서 로고 벡터만 떼어내고, 흰색으로 바꾸고, 복붙해서 레이어블러 먹여서 그림자 만들고 배치한다음 대표색 따놓고 바꾸고 싶은건 바꿨습니다. 기존의 색에서 밝기를 높이고 채도를 낮추는 게 제 취향에 맞더라구요.
스타일드 컴포넌트는 로고가 별로 안예쁜 것 같아요
Vite는 노란 번개가 포인트인 것 같아서 남겨줬습니다 흰색으로 밀어버리면 별로길래
스킬 정보를 어떻게 관리할지가 판단이 잘 안서서 고생했습니다. 일단 리스트를 만들어두고 거기에서 보고싶은 걸 클릭하면 그걸 카드로 띄워줄 건데, 지금은 할 줄 아는게 쥐뿔도 없어서 아이템들이 적지만 점점 늘어나면 카테고리 별 분류가 필요할 것 같더라구요. 그래서 이렇게 했습니다.
(개중에는 배운지 얼마 안됐거나 굳이 스택에 써놓을만한 게 아닌 것도 있지만, 일단 최대한 많이 적는 게 UI 테스트하기에 좋을 것 같아서 이것저것 우겨넣었습니다 react-router-dom같은거 빼고)
이제 skills 가져오면 5개의 스킬셋을 가져오고, 그 안의 data 필드에는 각각의 스킬 정보들이 들어있습니다.
interface SkillListProps {
data: SkillSet[];
onItemClick: (skill: Skill) => void;
selectedSkill: Skill | null;
}
export default function SkillList({
data,
onItemClick,
selectedSkill,
}: SkillListProps) {
return (
<ul>
{data.map((skillSet: SkillSet) => (
<li key={skillSet.title}>
<section>{skillSet.title}</section>
<ul>
{skillSet.data.map((skill: Skill) => (
<li
key={skill.title}
onClick={() => onItemClick(skill)}
className={`cursor-pointer ${
selectedSkill && selectedSkill.title === skill.title
? "font-bold cursor-default"
: ""
}`}
>
{skill.title}
</li>
))}
</ul>
</li>
))}
</ul>
);
}
리스트 컴포넌트입니다. 각 아이템을 클릭해서 선택할 수 있고, 선택된 아이템은 볼드 표시합니다.
import { useEffect, useState } from "react";
import SkillList from "./SkillList";
import SkillCard from "./SkillCard";
import { Skill, SkillSet } from "../../types";
interface SkillContainerProps {
data: SkillSet[];
}
export default function SkillContainer({ data }: SkillContainerProps) {
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
useEffect(() => {
if (data.length > 0 && data[0].data.length > 0) {
setSelectedSkill(data[0].data[0]);
}
}, [data]);
return (
<section>
<section>Skills</section>
{selectedSkill && (
<SkillCard key={selectedSkill.title} skill={selectedSkill} />
)}
<SkillList
data={data}
onItemClick={(skill: Skill) => setSelectedSkill(skill)}
selectedSkill={selectedSkill}
/>
</section>
);
}
About 페이지에 띄워줄 SkillContainer 컴포넌트입니다. 선택된 스킬을 상태로 관리하고, 데이터를 불러올 때 selectedSkill을 첫 번째 스킬셋의 첫 번째 스킬로 설정합니다. 원래는 useState의 초기값을 그렇게 설정했는데, data가 아직 다 불러와지지 않은 상태에서 data[0].data[0]에 접근하려고 하니 타입에러가 뜨더라구요...
좀 깁니다... 코드 접기 기능같은거 없나요
export default function About() {
const darkMode = useDarkMode();
const {
data: profileData,
loading: profileLoading,
error: profileError,
} = useFetchDocument<Profile>("/about", "profile");
const {
data: skillsData,
loading: skillsLoading,
error: skillsError,
} = useFetchCollection<SkillSet>("skills");
return (
<div
className={`${
darkMode ? "text-white bg-slate-500" : "text-gray-700"
} transition-all duration-300 ease-in-out`}
>
<section>
<section className="mt-16">
<main className="flex flex-col-reverse md:flex-row md:justify-between w-full">
<section className="flex flex-col w-full p-10 ml-0 items-center md:ml-32 md:w-2/3">
<section>
{profileLoading && <div>Loading profile data...</div>}
{profileError && (
<div>
Failed to fetch profile data : {profileError.message}
</div>
)}
{profileData && (
<ProfileBox description={profileData.description} />
)}
</section>
<section>
{skillsLoading && <div>Loading skill data...</div>}
{skillsError && (
<div>Failed to fetch skill data : {skillsError.message}</div>
)}
{skillsData && <SkillContainer data={skillsData} />}
</section>
</section>
{profileData && (
<img
src={profileData.imgUrl}
alt={profileData.imgUrl}
className="rounded-xl min-w-[250px] m-6 md:w-1/5 md:m-0 md:h-auto md:rounded-none md:rounded-bl-[20%]"
/>
)}
</main>
</section>
</section>
</div>
);
}
useFetchCollection 훅으로 skills 콜렉션을 싸그리 불러오고, SkillContainer에 넣어줍니다... 구조가 좀 개판인 느낌인데 꾸미기 할 때 고생할 것 같네요... 그럼 또 블로깅할 거리가 생기겠죠 뭐...
하면서도 이게 맞나 싶은 순간이 너무 많아서 그렇습니다
한번쯤은 '와 나 천잰가?'싶은 경험들이 있어야 블로깅도 하고 이력서에 쓸 거리도 생길 텐데...
고민하고 해결할 만한 수준의 문제가 발생하지 않는 프로젝트를 하고 있어서 그런 걸까요
일단 잡니다... 내일은 월요일이니까요