오늘 한 일
- 알고리즘 문제 풀이
- 카공실록 개발
- MSW 세팅
- 카공 기록 바텀시트 UI 구현
- 부스트캠프 자소서 작성
카공실록 개발
MSW 세팅 문제
MSW를 사용하려고 했다..
근데 어제 TIL 쓰고 좀 찾아보다가 이 discussion을 발견했다.
Next13의 App directory를 지원하지 않는다는 내용이다. 혹시라도 우회해서 할 수 있는 방법이 없나 열심히 찾아보았다.
다른 연합 동아리에서 진행하고 있는 프로젝트도 찾아보니 Next.js 13을 사용하면서 MSW를 사용하고 있는 프로젝트가 있었다. 그 팀들은 아직 개발 초기라 기본 세팅만 되있는 상태였다.
하지만 자세히보니 msw/node만을 사용하고 있었고, msw/browser는 사용하지 않고 있었다. msw를 mock 테스트 용으로만 사용하고 있었다.
내가 원했던건 msw/browser를 사용해서 api를 개발 환경에서 모킹해서 사용하는 것이었다. 그래서 직접 msw/browser를 사용해서 세팅을 해보려고 했다.
어찌어찌 동작하게 할 순 있었지만, 여러 에러가 많이 터졌다. 어쩔 수 없이 msw를 사용하지 않기로 결정했다.
대신 json 형태로 더미데이터를 저장해두어 get 요청으로 사용하는 방식으로 개발을 진행하기로 했다.
fetching tool
난 항상 axios로 HTTP 요청을 보내왔다. 그런데 Next.js 13에서는 내장 함수인 fetch를 사용하도록 권장하고 있다.
그리고 이걸 사용해야 SSR, SSG 이런 렌더링 방식을 사용할 수 있다고 한다.
// app/page.tsx
export default async function Page() {
// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
// `force-cache` is the default and can be omitted.
const staticData = await fetch(`https://...`, { cache: 'force-cache' })
// This request should be refetched on every request.
// Similar to `getServerSideProps`.
const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
})
return <div>...</div>
}
axios는 지원하지 않고 있기 때문에, 다른 방안을 찾아보았다.
이것도 역시 다른 프로젝트들을 둘러봤는데 ky 라이브러리를 사용하고 있었다.
ky는 axios와 비슷한 라이브러리인데, fetch를 기반으로 만들어진 라이브러리이다. 그래서 Next.js 13에서도 사용할 수 있는 것 같다.
axios의 방식
import axios from 'axios'
import type { PlaceType } from '@/types/place'
export const getPlace = async (id: string) => {
const { data } = await axios.get<PlaceType>('/db/place.json')
return data
}
ky의 방식
import ky from 'ky'
import type { PlaceType } from '@/types/place'
export const getPlace = async (id: string) => {
const data = await ky.get('/db/place.json')
return data.json<PlaceType>()
}
@toss/ky 를 사용할까?
ky는 browser 환경에서 사용하기 위해 만들어진 라이브러리이다. 그래서 node 환경에서는 ky-universal을 사용해야 한다. 이걸 한 번에 해결해주는 라이브러리가 @toss/ky이다.
@toss/ky의 설명
왜 필요한가요?
ky 라이브러리는 아래와 같은 문제를 가지고 있습니다.
ESM-only 라이브러리이기 때문에 CJS로 require() 하려고 하는 경우 오류가 발생합니다.
SSR 대응을 위해서는 ky-universal 라이브러리를 사용해야 합니다.
@toss/ky는 ky 를 쉽게 사용할 수 있도록 이를 개선합니다.
ESBuild로 한 차례 빌드함으로써 CJS-ESM 모두에서 사용할 수 있도록 하고,
서버에서도 ky-universal을 쓸지, ky 를 쓸지, 구분할 필요 없이 @toss/ky 만 쓰면 되도록 합니다.
그래서 @toss/ky를 사용하기로 했는데, 한 가지 문제가 있었다.
@toss/ky에는 ky의 타입을 내장하고 있지 않았다. 혹시나 DefinetlyTyped에 ky의 타입이 있을까 찾아봤는데, 없었다.
그래서 그냥 ky를 사용하기로 했다.
카공기록 바텀시트 UI 구현
여기서 가운데 보이는 스크롤 가능한 DatePicker는 아직 구현하지 않았다. 마땅한 라이브러리를 못 찾아서 그냥 내가 만들어야 할 것 같다.
이미지를 업로드하는 부분이 저번에 만들었던 리뷰등록 바텀시트에서도 존재했어서, ImageUpload라는 컴포넌트로 분리해서 재사용하도록 했다.
import Image from 'next/image'
interface ImageUploadProps {
images: File[]
onUpload: (imageFiles: File[]) => void
className?: string
}
export default function ImageUpload({ images, onUpload, className }: ImageUploadProps) {
const handleUpload = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.multiple = true
input.onchange = (e) => {
const files = Array.from((e.target as HTMLInputElement).files!).filter((file) => {
for (const image of images) {
if (file.name === image.name && file.size === image.size) return false
}
return true
})
if (images.length + files.length > 5) {
alert('이미지는 최대 5개까지 업로드할 수 있습니다.')
return
}
onUpload([...images, ...files])
}
input.click()
}
return (
<div className={`flex w-[calc(100%+1.5rem)] gap-2 pr-6 ${className ?? ''}`}>
<div
className="flex h-[72px] w-[72px] shrink-0 cursor-pointer flex-col items-center justify-center bg-background"
onClick={handleUpload}
>
<Image src="/assets/icons/32/Camera.svg" alt="camera" width={32} height={32} />
<p className="text-caption">
{images.length}
<span className="text-bk40">/5</span>
</p>
</div>
<div className="flex gap-2 overflow-hidden overflow-x-scroll">
{images.map((image, index) => (
<div key={index} className="relative h-[72px] w-[72px] flex-shrink-0">
<Image src={URL.createObjectURL(image)} alt="image" className="object-cover" fill />
<Image
src="/assets/icons/16/Delete.svg"
alt="close"
className="absolute right-1 top-1 cursor-pointer"
width={16}
height={16}
onClick={() => {
onUpload(images.filter((_, i) => i !== index))
}}
/>
</div>
))}
</div>
</div>
)
}
이미지를 업로드하는 부분은 대충 이런 식으로 작성하면 된다.
import { useState } from 'react'
import ImageUpload from '@/components/ImageUpload'
export default function ReviewBottomSheet() {
const [images, setImages] = useState<File[]>([])
return <ImageUpload images={images} onUpload={(imageFiles) => setImages(imageFiles)} />
}
이미지가 최대 5개까지 들어가야 해서 제한을 걸어두었다. 그리고 이미지의 우측 상단에 X 버튼을 누르면 filter 메서드를 통해 해당 index의 이미지를 제외한 나머지 이미지들을 반환하도록 했다.
내일 할 일
- 알고리즘 문제 풀이
- 카공실록 갤러리 페이지 구현
- 리뷰 상세 페이지 구현
- 소프트웨어공학 시험 공부