⭐ 소원을 빌고 송편을 받자 ⭐
달나라에 사는 토끼 요정 묘정이가 특별한 송편을 만들어줍니다!
나를 위해, 혹은 다른 사람을 위해 소원을 빌어보세요🙏
이번 글은 2023년 9월 11월부터 27일까지 약 2주간 진행한 묘정송편
프로젝트에 대해
개발 사이클을 기반으로 한 회고이다.
글의 말단에서는 프로젝트에 대한 KPT 회고도 포함시켰다.
요구사항 분석이면서 프로젝트를 시작하게된 계기는 다음과 같다.
프로지방러는 9월 초부터 추석 때 서울에서 지방으로 가는 KTX를 예매하느라 추석이 곧 다가오는 걸 알 수 있었다. 그래서 문득 이벤트성 웹사이트를 만들면 어떨까하는 생각이 들었고,
내가 어떤 문제를 해결할 수 있을지 고민을 하게됐다.
주요하게 생각했던 포인트는 다음과 같다.
이런 포인트를 고려하면서 내가 이맘때, 그리고 벌써 한 달이 지난 지금까지도 가지고 있는 문제는 '심신이 지쳤다'
는 것이다. 내 생일이 있는 9월이지만서도 육체적으로, 정신적으로 좋지 않은 시기였다.
그래서 '어떤 부분이든 결핍을 가지고 있는 것을 사람들끼리 서로 위로할 수 있는 서비스'를 주된 방향으로 잡았다.
추석이라는 이벤트를 위주로 브레인스토밍을 한 결과,
올해 신묘년의 토끼🐰, 추석 전통 음식의 송편🍡이 합쳐서 '정을 나누는 토끼, 묘정이의 송편'이라는 컨셉이 구체적으로 잡혀지게 되었다.
아이디어가 반짝! 떠올랐을 때는 백엔드도 내가 전부 해볼까 싶었는데, 기획도 구체적으로 나오지 않았고, 기한 내에 백엔드 서버 배포까지 하기는 어려움이 있다고 생각해서 전 직장 동료분께 SOS 요청을 했다.
그리고 흔쾌히 함께 하겠다고 하셔서 서둘러 동료로 섭외했다!
감사해요 백엔드 마스터~!
그래서 같이 기획부터 디테일하게 잡고 가면 좋을 것 같아서
만나서 유저 플로우를 짜면서 추가하고 싶은 기능과 API 기준으로 디테일을 잡아나갔다.
역시 백엔드를 많이 해본 사람의 관점으로 볼 때 집중하는 부분이 달라서 섭외하길 정말 잘했다는 생각이 들었다.
예를 들면 소원을 작성하는 사람의 이름을 unique하게 가져갈 것인가, 소원마다 password를 걸어서 비공개 처리를 할 것인가와 같은 미처 생각지도 못한 부분들을 집어주셨다!
기획도 그에 맞춰서 기능이 추가되고 처음에 러프하게 생각했던 플로우에서 구체화시킬 수 있었다.
추가하고 싶은 기능들은 계속 생겨났지만,
디자이너가 할 일도 내가 하게 돼서, 일이 두 배로 늘어나지 않기 위해 자제했다.
그래서 대략적으로 나온 화면을 와이어프레임으로 작성하면서 동시에 기능에 걸맞는 기술 스택을 조사하고 정한 뒤 초기 설정을 병행했다.
프론트엔드를 담당하면서 동시에 디자인을 하는 동안, 댜음과 같은 부분들을 유념하면서 작업했다.
프로젝트 규모가 큰 편은 아니라서 고민하는 데에 있어서 많은 시간이 걸리진 않았다.
오히려 더 컸으면 디자인 시스템을 직접 구축해보는 데 있어서 좋은 경험이 될 수도 있었을텐데라는 아쉬움도 들었다.
아무쪼록 위와 같은 부분을 신경쓰면서 동시에 코드에 녹여낼 수 있도록 다음과 같이 Button.tsx
라는 컴포넌트를 생성했다.
커스텀이 필요한 사이즈(너비, 높이, 폰트크기)와 색(테두리색, 버튼색)을 type과 color의 prop에 따라 결정되도록 구성했다.
다소 오버 엔지니어링으로 볼 수 있겠지만, 당시에 와이어프레임을 작성할 때도 기능 추가 및 페이지 추가가 유연하게 될 수 있도록 하기 위해 버튼 별로 따로 컴포넌트를 제작하지 않고 커스텀할 수 있도록 만들었다.
위의 방식에서 Styled Component의 props로 임의의 값을 내려줄 때, 네이밍이 DOM attribute에 해당하지 않아서 발생하는 오류였다.
render가 안되는 것은 아니지만, console에 뜨는 log라서 그냥 지나칠 수 없었다. 그래서 warning에 나와 있는대로 shouldforwardprop
을 설정해서 해결했다.
How do I provide props to styled elements in Typescript?
What is the purpose of shouldForwardProp option in styled()?
다시 보니 캐릭터 디자인이라고 하기에 너무 민망하군...
작업 순서로 따지자면 사실 캐릭터 디자인이 가장 나중에 완료한 것이지만,
디자인 섹션에 포함시켜서 이야기한다.
전문 일러스트레이터는 아니지만 그림 그리는 걸 좋아해서 조금씩 그려왔었는데,
그게 가끔씩 프로젝트할 때 도움이 되곤 한다.
아래 그림은 그냥 냅다 자랑하기!
이번에도 사실 조금 더 다양한 묘정이를 그리고 싶었으나 감기몸살 + 서류 지원 + 클라우드 교육 시작 + 매주 코딩테스트 콤보로 시간을 많이 내지 못했다.
TMI 1. og tag의 미리보기로 들어가는 이 이미지는 추석 전 날 집으로 가는 여정 중, 지하철에서 그렸다.(아마 1호선 군포역을 지나갈 때 쯤...)
아무튼 최종 묘정이는 [완전 꼬질/덜 꼬질/깨끗] 중에서 덜 꼬질
이 팀원의 만장일치로 선정이 되었다.
끼얏호!
그래도 익숙해져서 그런지 덜 꼬질 묘정이
가 가장 귀엽다!
React query
소원을 보여주는 목록 페이지에서 무한 스크롤 처리와 POST 요청 유무에 따라서 페이지 분기 처리를 하는 데 있어서 빠르게 하기 위해 도입했다.
React spring
10 Best Javascript Animation Libraries to Use in 2023 이 글을 읽고 나서 페이지 애니메이션을 적용하기 용이하다고 하여 도입했다.
Recoil
뒤에서 Funnel 구조를 이야기할 때 한 번 언급하겠지만, 필요할 줄 알아서 미리 add 해 놓은 것을 추후 uninstall하게 되었다! 조금이나마 빌드 용량을 줄였다고 볼 수 있지 않을까?
Bun
Why You Should Use Bun 이 글을 읽고 나서 빠르면 얼마나 빠르길래! 싶어서 바로 패키지 매니저로 도입했다.
Home brew bun install
brew tap oven-sh/bun
brew install bun
App Initial
bun create react-app . --template typescript
보이다시피 yarn에 비해 초기 install은 그렇게 빠른지 잘 모르겠다. 내가 잘못 파악해서 그런가, 아니면 다른 쪽에서 더 빠른 건가, 이 부분은 더 공부해야할 것 같다.
Add Libraries
bun add -d eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier eslint-plugin-import eslint-import-resolver-typescript
eslint
: ESLint core library@typescript-eslint/parser
: a parser that allows ESLint to understand TypeScript code@typescript-eslint/eslint-plugin
: plugin with a set of recommended TypeScript rules[eslint-config-prettier](https://github.com/prettier/eslint-config-prettier)
to disable all ESLint rules that are irrelevant to code formatting, as Prettier is already good at it:eslint-plugin-import
<참고 자료>
How to use ESLint with TypeScript | Khalil Stemmler
Linting in TypeScript using ESLint and Prettier - LogRocket Blog
eslint-plugin-import
ESLint 알고 쓰기
3 Ways To Use Bun With Create React App
다른 라이브러리를 추가적으로 install할 때는 조금 더 빠른 것 같기도 하다!
항상 프론트 코드만 똥땅거리다가 인프라를 처음부터 해보는 것을 처음이라... 매우 떨렸다.
하지만 AWS에서 하라는대로 하면 되겠지~라는 마음으로 차분히 따라가보았다.
휴 앱을 설정하는 것까지는 문제가 없다!
그런데 초기 설정되어 있는 빌드 명령어를 보고...
'앗, 나는 bun을 쓰는데 그러면 명령어로 bun으로 바꿔야 하는 거 아냐?'
이후 이어지는 잘못된 build 명령어와 build failed 메시지...
이건 관련된 기술블로그나 참고할 만 한 게 없어서 bun 공식문서와 amplify 빌드 문서를 읽고 이것저것 시도해보면서 답을 찾았다.
쨔란
본격 틀린 그림 찾기! 그것은 바로바로...
예... 제가 버지니아 북부까지 갈 줄 누가 알았겠어요...
앱을 그대로 가져가서 지역만 변경할 수 있는 방법이 있나 찾아보았지만, 별 방법이 없어서, 다시 한국 region으로 앱을 재생성하고 지웠다!
이 단계 쯤에서 생각보다 일정이 딜레이가 돼서 동료를 한 분 더 섭외했다!
감사합니다, 프로 밤샘러 프론트~!
이 분은 묘정송편의 핵심인 달나라🌝를 전부 작업해주셨습니다. 👍
토스ㅣSLASH 23 - 퍼널: 쏟아지는 페이지 한 방에 관리하기
언젠가 toss의 기술 컨퍼런스에서 위 영상을 본 적이 있다. 항상 토스 서비스를 이용하면서 반복적으로 나오는 페이지 구조를 User 친화적으로 잘 구성했다고 생각했다.
이런 페이지 유형을 Funnel이라고 했고, Toss는 해당 페이지 구조를 재사용성, 관심사 분리 측면에서 코드 설계를 한 사례를 공유하기도 했다.
인상깊게 봤기 때문인지, 페이지 와이어프레임을 짠 뒤로 바로 Funnel이 생각났다.
찾아보니 커스텀훅으로 쓸 수 있도록 라이브러리로 만든 것 같다.
그런데 굳이 라이브러리를 쓰진 않고, 코드 구조를 벤치마킹하여 우리의 Story 레이아웃을 Funnel 구조로 구현했다.
장점
단점
Amplify로 브랜치 별로 배포 도메인을 분리할 수 있어서 E2E 테스트를 하기에 정말 좋은 환경이었다.
그래서 develop에 merge 하면서 꾸준히 여러 기능들을 테스트 해보았는데...
안드로이드 유저: 카카오톡으로 받은 링크에서 소원을 작성했는데 링크 복사나 공유가 안돼요!
아놔, 아이폰 유저는 생각지도 못한...
그래서 추석 때 본가에 가서 어머니 휴대폰을 잠시 빌려서 테스트를 해 본 결과,
크롬에서는 잘 작동되지만, 안드로이드 + 카카오톡 인앱 브라우저
의 경우 링크 복사가 안되는 것을 찾았다.
그래서 다들 카카오톡 공유하기 api를 쓰는 거였나...
이 문제는 카카오톡 인앱 브라우저 자체의 문제이기 때문에 공유하기 로직에서 이를 처리하기 보다는, 처음 앱을 접근했을 때부터 사용자의 브라우저 환경에 따라 외부 브라우저로 튕기도록 하는 것이 가장 적합하다고 생각해서 index.html
을 건드렸다!
<script type="text/javascript" charset="UTF-8">
var inAppDeny = (callback) => {
if (document.readyState !== "loading") {
callback();
} else {
document.addEventListener("DOMContentLoaded", callback);
}
};
inAppDeny(() => {
/* Do things after DOM has fully loaded */
async function copyToClipboard(val) {
try {
await navigator.clipboard.writeText(val);
alert("URL 복사를 성공했어요.");
} catch (err) {
alert(
"URL 복사를 실패했어요. 직접 복사해서 다른 브라우저가 열리면 주소창을 길게 터치한 뒤, '붙여놓기 및 이동'를 누르면 정상적으로 이용하실 수 있어요."
);
}
}
function inAppBrowserOut() {
copyToClipboard(window.location.href);
location.href = "x-web-search://?";
}
var userAgent = navigator.userAgent.toLowerCase();
var target_url = location.href;
if (userAgent.match(/kakaotalk/i)) {
//카카오톡 외부브라우저로 호출
location.href =
"kakaotalk://web/openExternal?url=" + encodeURIComponent(target_url);
} else if (userAgent.match(/line/i)) {
//라인 외부브라우저로 호출
if (target_url.indexOf("?") !== -1) {
location.href = target_url + "&openExternalBrowser=1";
} else {
location.href = target_url + "?openExternalBrowser=1";
}
} else if (
userAgent.match(
/inapp|naver|snapchat|wirtschaftswoche|thunderbird|instagram|everytimeapp|whatsApp|electron|wadiz|aliapp|zumapp|iphone(.*)whale|android(.*)whale|kakaostory|band|twitter|DaumApps|DaumDevice\/mobile|FB_IAB|FB4A|FBAN|FBIOS|FBSS|trill|SamsungBrowser\/[^1]/i
)
) {
//그외 다른 인앱들
if (userAgent.match(/iphone|ipad|ipod/i)) {
//아이폰은 강제로 사파리를 실행할 수 없다
//모바일대응뷰포트강제설정
var mobile = document.createElement("meta");
mobile.name = "viewport";
mobile.content =
"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, minimal-ui";
document.getElementsByTagName("head")[0].appendChild(mobile);
} else {
//안드로이드는 Chrome이 설치되어있음으로 강제로 스킴실행한다.
location.href =
"intent://" +
target_url.replace(/https?:\/\//i, "") +
"#Intent;scheme=http;package=com.android.chrome;end";
}
}
});
</script>
카카오톡 일해라잇!!
두근두근 역사적인 순간!
추석 전까지 불태운 서비스... 집에 가서도 쉬지 못하고 머리 쥐어싼 프로젝트...
홍보를 하면서 좋은 말씀을 많이 해주셔서, 힘들었던 것을 다 잊어버렸다.
예상했던 것과 달리 도메인이 가장 비쌌다! 이 도메인으로 다른 프로젝트를 또 해볼까 싶다.
폰트는 내 글씨체다!
여기서 체험할 수 있어요!
구현 중에 토스가 유사한 컨셉으로 먼저 이벤트를 출시했다..
그...그치만 우리 묘정이 더 귀여워!!!!