설날 휴일 첫째날인 오늘.. 아침에 심각한 고민에 빠졌다
게임하기에는 다 재미가 없고 뭔가 만들자니 애매한데.. 그렇게 유튜브랑 허공을 보다가 2시간이 흐르고 오전 10시가 되었다
github의 READMD.md페이지를 보면서
"아 ! 이거 ! 뱃지 생성기 만들려고 했는데 지금 하면 되겠네!"
머리에 스치는 스택은 대충
JSDOM
htmltoimage
이런 것들만 생각이 나는데..
몇 주 전인가 nextjs 도큐먼트를 재미 삼아 보면서 발견한 기능이 있었는데 그건 바로..
https://nextjs.org/docs/app/api-reference/functions/image-response
사실 이번 아이디어도 저거보다 나온 거다
그래서 "오케이 nextjs 단독으로 함 작성해 보자"
그렇게 메인 스택이 nextjs, UI 짜면 하루정도 더 걸리니까
그냥 이미 다 짜져 있는 shadcn을 쓰기로 한다
흑흑 사랑해요 shadcn.. 너무 이뻐..
일단 nextjs와 shadcn을 초기화한다
nextjs는 이미 다 인터넷에 올라와있고..
shadcn도 인스톨 한 뒤에
Bash
npx shadcn-ui@latest add
치고 a 누르면 전체 선택.. 그리고 엔터 치면 바로 전체 사용이 가능하다
일단 페이지를 대충 슥슥 그리고
오늘의 메인 기능은 이미지 뽑는 api를 nextjs에서 작성해 보자
app폴더 아래에 api폴더를 하나 작성해 주고
그 아래에 subpath가 될 폴더를 또 작성해 준다 (나는 icon으로 작성)
그리고 tsx파일을 route.tsx로 작성해준다
JavaScript
import { PROPERTIES, PropertyMap } from '@/lib/constant'
import { headers } from 'next/headers'
import { ImageResponse } from 'next/og'
import { NextRequest } from 'next/server'
export const runtime = 'edge'
export const GET = async (request: NextRequest) => {
const headersList = headers()
const domain = headersList.get('x-forwarded-host')
const origin = headersList.get('x-forwarded-proto')
const currentURL = `${origin}://${domain}`
const { searchParams } = new URL(request.url)
const { width, height, icon, cIcon, iconBgColor, bgColor, textColor, text, borderRadius } = Object.keys(PROPERTIES).reduce(
(prev, next) => ({ ...prev, [next]: searchParams.get(next) }),
{},
) as PropertyMap
const props = {
width: Number(width) || 100,
height: Number(height) || 25,
bgColor: bgColor ? bgColor : 'transparent',
textColor: textColor ? textColor : '#000',
iconBgColor: iconBgColor ? iconBgColor : 'transparent',
text: text || '',
icon: cIcon || `${currentURL}/${icon}.svg` || `${currentURL}/logo.svg`,
borderRadius: (Number(borderRadius) || 0) + 'px',
}
return new ImageResponse(
(
<div
style={{
width: props.width + 'px',
height: props.height + 'px',
backgroundColor: props.bgColor,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'baseline',
borderRadius: props.borderRadius,
overflow: 'hidden',
}}
>
<div
style={{
backgroundColor: props.iconBgColor,
width: props.height + 'px',
height: props.height + 'px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */}
<img
src={props.icon}
style={{
objectFit: 'contain',
width: props.height - props.height / 5,
height: props.height - props.height / 5,
marginLeft: props.height - props.height / 1.5,
}}
/>
</div>
<span
style={{
color: props.textColor,
textAlign: 'center',
flex: 1,
justifyContent: 'center',
fontSize: props.height / 1.5 + 'px',
}}
>
{props.text}
</span>
</div>
),
{
width: props.width,
height: props.height,
fonts: [
{
name: 'M PLUS Rounded 1c',
data: await fetch(new URL(currentURL + '/mplus.ttf', currentURL)).then((res) => res.arrayBuffer()),
weight: 100,
style: 'normal',
},
],
},
)
}
전문을 긁어오긴 했는데 하나씩 떼서보자
먼저
TypeScript
const headersList = headers()
const domain = headersList.get('x-forwarded-host')
const origin = headersList.get('x-forwarded-proto')
const currentURL = `${origin}://${domain}`
const { searchParams } = new URL(request.url)
const { width, height, icon, cIcon, iconBgColor, bgColor, textColor, text, borderRadius } = Object.keys(PROPERTIES).reduce(
(prev, next) => ({ ...prev, [next]: searchParams.get(next) }),
{},
) as PropertyMap
const props = {
width: Number(width) || 100,
height: Number(height) || 25,
bgColor: bgColor ? bgColor : 'transparent',
textColor: textColor ? textColor : '#000',
iconBgColor: iconBgColor ? iconBgColor : 'transparent',
text: text || '',
icon: cIcon || `${currentURL}/${icon}.svg` || `${currentURL}/logo.svg`,
borderRadius: (Number(borderRadius) || 0) + 'px',
}
각종 변수들을 싹 정리해 주자
next/header에서 제공하는 기능으로 header에서 domain과 origin을 뽑아서
굳이 env로 뺄 필요 없이 도메인을 자동으로 작성해 주자
두 번째로는 request에서 긁어온 url에서 searchParam을 싸악 긁어오자
reduce한방이면 object로 말아서 export 할 수가 있다
미리 들어올 object에 대한 type도 작성해 두었다
그리고 이제 render에서 쓰일 변수들을 싸악 다시 props로 재구성해준다
null처리랑 기본 값 등등.. 잘 선언해 주자
Bash
return new ImageResponse(
(
<div
style={{
width: props.width + 'px',
height: props.height + 'px',
backgroundColor: props.bgColor,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'baseline',
borderRadius: props.borderRadius,
overflow: 'hidden',
}}
>
<div
style={{
backgroundColor: props.iconBgColor,
width: props.height + 'px',
height: props.height + 'px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */}
<img
src={props.icon}
style={{
objectFit: 'contain',
width: props.height - props.height / 5,
height: props.height - props.height / 5,
marginLeft: props.height - props.height / 1.5,
}}
/>
</div>
<span
style={{
color: props.textColor,
textAlign: 'center',
flex: 1,
justifyContent: 'center',
fontSize: props.height / 1.5 + 'px',
}}
>
{props.text}
</span>
</div>
),
{
width: props.width,
height: props.height,
fonts: [
{
name: 'M PLUS Rounded 1c',
data: await fetch(new URL(currentURL + '/mplus.ttf', currentURL)).then((res) => res.arrayBuffer()),
weight: 100,
style: 'normal',
},
],
},
)
이번 글의 하이라이트인 ImageResponse.. 라기엔 너무 초라하긴 하다
그냥 HTML로 써서 사진으로 나타낼 정보를 그려주자
각 값들은 props, 위에서 구조분해 할당으로 선언한 변수들로 값을 채워주면 api/icon에 대한 라우팅은 끝이다
실제로 postman으로 값 찍어보면 이렇게 나온다
이제 이 라우터를 가지고 nextjs에서 페이지를 작성하면 된다
이다음부터는 shadcn과 기초적인 react이기에 따로 설명은 필요 없고.. github페이지 하나 남겨둔다
https://github.com/B-HS/BIcon/blob/main/app/page.tsx
아침부터 10시간? 정도 삽질하고 작성하고 중간에 밥도 먹고 술도 먹고..
생각했던 것이 하루 만에 끝내서 재밌게 한 프로젝트인듯하다
shadcn이 막대한 역할을 했고..
하마터면 JSDOM과 htmlToImage로
또 다른 삽질 할 것을 막아준 nextjs에게 무한한 감사..
모든 소스코드는 아래에 있으니 관심이 있으면 들러주시고 별 하나 박아주시면 감사합니다..