← 좋아요 👍 와 댓글 💬 은 큰 응원이 됩니다!
@next/link
를 이용해 제작했습니다.Tech Type | Tech Stack |
---|---|
Front-end | TypeScript React (Next) |
Front-end Server | Vercel |
DNS | AWS Route 53 with CloudFlare |
Database | Static assets hosted on Vercel |
Analytics | Google Analytics |
최근 경쟁률이 매우 높은 (경쟁률 20:1 내외) 교내 기술창업 동아리를 지원하는데 다음과 같은 질문이 있었습니다. 여기서 meme이란 '웃음을 유발하는 짧은 동영상'과 같은 뜻입니다.
반드시 들어가고 싶었던 동아리였기에 많은 고민이 들었습니다. 사람마다 관심사와 웃음 코드가 다르며 특정 그룹에게는 웃긴 내용이 다른 그룹에게는 불쾌한 내용으로 다가갈 수 있기 때문입니다.
그러다 "차라리 선택에 기반해 meme을 추천해주는 서비스를 만들면 어떨까?"하는 생각이 들었습니다. 반응형 추천 시스템(새로 누적되는 데이터에 기반해 추천 내용이 동적으로 변하는 시스템)이 아니기 때문에 기술적인 복잡도도 그렇게 높지 않을 것 같았습니다. 마침 주말이 있었기 때문에 빠르게 만들어보기로 결정했습니다.
우선 제가 재밌게 본 동영상들을 리스트업했습니다. (틱톡, 트위터, 인스타그램 대신 유튜브를 사용해도 상관 없을 것이라 판단했습니다.) 단톡방에서 youtu
키워드로 검색해서 제가 웃기다고 생각한 동영상들을 쉽게 찾을 수 있었습니다.
이후 Notion 페이지에서 카테고리별로 분류했습니다. 음악, 영화, 게임, 코딩, 일반적 Meme으로 나눌 수 있었습니다.
선택 기반 추천 테스트를 구상하다 보니 2가지 접근 방법이 있는 것 같았습니다. 하나는 질문의 답변마다 가중치를 주고, 최종 점수를 계산하여 결과를 추천해주는 시스템입니다. 나머지 하나는 전체적인 시나리오 트리를 설정해두고 선택지의 조합에 따라 결과를 추천해주는 것입니다. 요즘 유행하는 MBTI 결과물들을 분석해본 결과 대개 첫 번째 점수 기반 추천 시스템을 사용했습니다. 저는 두 번째 시나리오 트리 기반 추천 시스템을 사용했습니다. 이유는 다음과 같습니다.
E/I
, N/S
, T/F
, J/P
이렇게 4개의 플래그만 존재하기 때문에 점수 상태관리를 하기 비교적 편리합니다.undo
액션을 추가하려면 더 많은 엔지니어링이 투입되어야 합니다.undo
가 됩니다.프론트엔드에 있어서는 별다른 고민을 하지 않았습니다. 최근 TypeScript Next와 사랑에 빠졌기 때문에 자연스럽게 선택하게 되었고 Vercel과 Next의 호환성을 알기에 Vercel에 호스팅하기로 결정했습니다. 스타일은 styled-component를 사용했습니다.
데이터를 어디에 저장할지에 대한 부분이 문제였습니다. Meme에 대한 데이터는 동적인 데이터가 아니고, 유저 정보는 저장할 일이 없다고 판단했기에 DB 또는 DBaaS를 따로 사용하는 대신 모든 데이터를 모듈화하여 하드코딩하기로 결정했습니다. 여기에서 하드코딩된 정보들을 보실 수 있습니다.
백엔드 또한 마찬가지로 구성할 필요가 없었습니다. 서버리스한 형태로 만들기로 했습니다.
다음과 같이 요약할 수 있습니다.
getStaticProps
와 getStaticPaths
를 사용해 반응 속도를 초고속으로 만듭니다.각 질문과 동영상은 다음과 같은 URI 구조를 가집니다.
https://smile.cho.sh/question/[id]
https://smile.cho.sh/video/[id]
TypeScript의 장점을 활용하여 type 구조를 미리 정의했습니다.
export type Question = {
id: number
contents: string[]
answers: Answer[]
}
export type Answer = {
id: number
content: string
result: number | null
nextQuestion: number | null
}
export type Video = {
id: number
title: string
uploader: string
desc: string
youtubeId: string
}
type Answer
에서 result
와 nextQuestion
은 둘 중 한 쪽만 값을 가질 수 있습니다. 이를 바탕으로 링크를 생성합니다. 이렇게 2가지로 별도의 필드를 통해 question
과 video
를 혼동하는 실수를 방지할 수 있었습니다. 또한 데이터를 작성할 때 기본값을 0
으로 두어 의도치 않은 null
오류를 방지하고자 했습니다. 그 흔적은 /question/0에서 확인하실 수 있습니다.
예를 들어 /question/[id]
에 해당하는 페이지들은 다음 코드를 통해 빌드 타임에 정적으로 생성됩니다.
export const getStaticPaths: GetStaticPaths = async () => {
const paths = questionData.map((question) => ({
params: { id: question.id.toString() },
}))
return { paths, fallback: false }
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const id = params?.id
const item = questionData.find((data) => data.id === Number(id))
return { props: { item } }
} catch (err) {
return { props: { errors: err.message } }
}
}
여기서 getStaticPaths
는 정적으로 생성되어야 할 페이지들의 path
들의 리스트를 정해주며, getStaticProps
는 path
와 일치하는 질문 데이터를 검색해서 React App에 props
형태로 전달합니다. 이를 통해 모든 질문과 동영상 페이지를 정적으로 미리 생성해둘 수 있습니다. 더 나아가 next/link
의 <Link>
까지 조합하여 활용한다면 페이지들을 prefetch
해올 수 있기 때문에 인터랙션을 초고속으로 만들 수 있습니다. (말 그대로, 브라우저 패비콘에서 로딩 도는 것도 보이지 않습니다!)
다시 말해서 인트로 페이지와 엔딩 페이지를 만들고, 빼먹은 디테일을 추가하는 일입니다. 예를 들어 특수한 경우를 위해 다른 형태의 View를 처리하는 작업을 했습니다. 사용자가 모든 질문에 대해서 잘 모른다고 답할 경우 다음과 같은 결과를 보여줍니다. 다른 뷰들은 동영상을 바로 embed
하는데 반해 이 경우에만 버튼의 형태로 보여주었습니다.
무슨 영상인지는 직접 확인해보세요!
ES6+
의 map
함수를 정말 자주, 잘 사용한 것 같습니다!
역시 갇Anaclumous님... 넥스트까지 섭렵해버리셨군여 ㅎㅎ 좋은 포스트 잘 보고 갑니다👍👍