개인 포트폴리오 웹 구현 프로젝트입니다.
프로젝트: 개인 포트폴리오 웹사이트
기획, 디자인, 제작: 채하은
분류: 개인 프로젝트
배포일: 2023.06.01
주요 기능:
- 다크모드
- 반응형 웹
- About / Project 데이터 동적 생성
- 프로젝트 더보기 기능
https://portfolio-49c62.web.app/
-
[2023.06.01] 사이트 배포
-
[2023.07.21] 접근성 향상 (태그 명암비 수정)
-
[2023.08.23] 사이트 성능/seo 개선
-
[2023.09.01] 프로젝트 더보기 기능 추가
const [toggle, setToggle] = useState('Dark')
const toggleDarkMode = () => {
if (localStorage.getItem('theme') === 'dark') {
localStorage.removeItem('theme')
document.documentElement.classList.remove('dark')
setToggle('Light')
} else {
document.documentElement.classList.add('dark')
localStorage.setItem('theme', 'dark')
setToggle('Dark')
}
}
useEffect(() => {
if (localStorage.getItem('theme') === 'dark') {
document.documentElement.classList.add('dark')
setToggle('Dark')
} else {
setToggle('Light')
}
}, [])
tailwind css 기능을 사용해 sm(640px)
md(768px)
lg(1024px)
xl(1280px)
을 기준으로 브레이크포인트를 잡았다.
컴포넌트 재사용이라는 리액트의 특징을 살리기 위해 firestore에 데이터를 저장한 뒤 동적으로 컴포넌트를 생성. firestore에 데이터 값을 입력해 넣으면 동적으로 데이터가 뿌려지게 된다.
같은 컴포넌트를 사용하면서도 데이터들의 타입에 따라 li 스타일에 차이를 둘 수 있도록 코드를 작성했다.
export const getProjectData = async (
itemsPerPage: number,
lastDoc: QueryDocumentSnapshot<DocumentData> | null,
) => {
const projectQuery = query(
collection(dbService, 'project'),
orderBy('id', 'desc'),
...(lastDoc ? [startAfter(lastDoc)] : []),
limit(itemsPerPage),
)
const querySnapshot = await getDocs(projectQuery)
const lastDocument = querySnapshot.docs.length
? querySnapshot.docs[querySnapshot.docs.length - 1]
: null
const dataQuery = querySnapshot.docs.map(doc => doc.data() as ProjectDataType)
return {
data: dataQuery,
lastDocument,
}
}
///
const [lastDoc, setLastDoc] = useState<LastDoc>(null)
const [hasMoreData, setHasMoreData] = useState<boolean>(true)
const loadMoreData = async () => {
try {
if (!hasMoreData) return
const result = await getProjectData(ITEMS_PER_PAGE, lastDoc)
if (result.data.length < ITEMS_PER_PAGE) {
setHasMoreData(false)
}
setProjectData(prevData => [...prevData, ...result.data])
setLastDoc(result.lastDocument)
} catch (error) {
openModal('데이터를 불러오는데 실패했습니다.')
}
}
한 번에 몇 개의 데이터를 보여줄지를 정하는 ITEMS_PER_PAGE
와 불러온 데이터의 마지막 데이터를 의미하는 lastDoc
를 인자로 받아 getProjectData
함수를 호출하고, 만약 불러온 데이터의 개수가 ITEMS_PER_PAGE
보다 작다면 hasMoreData
를 false로 변경해 더 이상 데이터를 불러오지 않도록 설정했다. 그리고 hasMoreData
가 true일 때만 loadMoreData
함수를 호출하도록 설정했다.
1. 타입에러
- 임시로 지정해놓은 데이터 타입 any를 수정하는 과정에서 무한 타입에러와 마주함.const [aboutData, setAboutData] = useState<any>([])
를
const [projectData, setProjectData] = useState<DocumentData[]>([]); // DocumentData[]는 firestore 자체에서 지원하는 타입
로 수정했으나,
'(data: DataType, index: number) => JSX.Element' 형식의 인수는 '(value: DocumentData, index: number, array: DocumentData[]) => Element' 형식의 매개 변수에 할당될 수 없습니다. 'data' 및 'value' 매개 변수의 형식이 호환되지 않습니다. 'DocumentData' 형식에 'DataType' 형식의 projects, date, description, techStack 외 2개 속성이 없습니다.ts(2345) (parameter) data: DataType
라는 에러와 함께 여전히 문제가 해결되지 않았다.
원인을 찾아본 결과 DocumentData 형식은 DataType의 필수 속성인 projects, date, description, techStack 외에 다른 속성을 갖지 않기 때문이었다.
interface DataType extends DocumentData {
projects: string
date: string
description: string
techStack: string[]
tag: string[]
github?: string
notion?: string
imgURL: string
deploy?: string
} //
DataType를 DocumentData를 상속하도록 확장하여 DataType이 DocumentData의 모든 속성을 포함하면서 추가적인 속성을 정의할 수 있게 변경했다.
const querySnapshot = await getDocs(projectQuery)
const dataQuery = querySnapshot.docs.map(doc => doc.data() as DataType)
또, dataQuery에서 doc.data()를 as DataType로 형변환하여 타입을 맞추어 data prop으로 사용되는 데이터의 타입을 DataType로 일치시켰다.
성능 개선 전
성능 개선 후
Vite는 프로덕션 빌드를 위해 기본적으로 특정 형태의 번들링을 하지 않기 때문에 별도로 이미지 최적화와 텍스트 압축을 통해 성능 개선 작업이 필요함을 깨달았다.
plugins: [
//...
compression(),
viteImagemin({
plugins: {
jpg: imageminMozjpeg(),
png: imageminPngQuant(),
gif: imageminGifSicle(),
svg: imageminSvgo(),
},
makeWebp: {
plugins: {
jpg: imageminWebp(),
png: imageminWebp(),
},
},
}),
],
vite-plugin-compression2
: Gzip 및 Brotli 압축 알고리즘을 지원하는 Vite 플러그인viteImagemin
: 이미지 최적화 플러그인
이 두 플러그인을 사용해 텍스트 압축과 이미지 최적화를 진행했다.
- 로딩스피너(스켈레톤)
- 프로젝트 filter 기능 (팀 프로젝트 / 개인 프로젝트 / 클론)
- 이력서 / 프로젝트 상세페이지 동적 라우팅
- seo 최적화