작년 5월 결혼을 하며, 모바일 청첩장을 처음부터 끝까지 직접 만들어 손님들께 인사드릴 때 사용했다. 결혼을 하기까지 참으로 많은 것들을 준비하고 계획해야 하는데, 그 중 기억에 남길 만한게 뭐라도 없을지 고민을 했다. 막 스튜디오 촬영을 마치고 견본 사진을 몇장 먼저 받았던 2022년 겨울, 아내(당시 여자친구)와 모바일 청첩장을 같이 만들기로 결정했다. 약간의 비용만 들이면 그럴듯한 청첩장을 얼마든지 간단하고 빠르게 만들 수 있지만, 디자인을 정하고 기획하면서 만드는 과정을 함께 하면 그것도 또 하나의 추억이 될 수 있겠다는 생각이 들었다. FE 개발자의 장기를 살려보고 싶다는 개인적인 욕심도 약간 있었고.
지나고 나면 남는건 사진 뿐이라는 어른들 말씀이 정말 틀린게 없었다. 이 글을 보시는 여러분들도 귀찮더라도 꼭 사진을 항상 열심히 찍으시길...
막상 만들고 나서 돌아보니, 후기를 따로 공유할 정도의 거창하거나 대단한 내용은 없었지만, 그래도 나름 사이드 프로젝트의 후기로서, 그리고 모바일 청첩장을 손수 만들고자 하는 어떤 분에게 약간의 참고가 되길 바라며 적어본다.
모바일 청첩장 링크
종이 청첩장을 만들 때 업체에 약간의 비용을 더 지불하면 간단한 모바일 청첩장도 같이 만들어주는 것이 보통이다. 아니면 모바일 청첩장만 전문으로 만드는 업체/서비스를 사용해도 되고. 해당 업체의 도메인 아래에 식별자가 추가된 형태로 모바일 청첩장이 서빙될텐데, 이러한 형태의 청첩장 사이트는 천년만년 접속할 수 있는 것이 아니다. 대게 예식이 있는날 전후로 유지되다가 바로 사이트가 만료된다.
통상적인 모청을 보며 가장 아쉽다고 생각한 것이 그 부분이었다. 내 생각에 모바일 청첩장은, 당사자들의 주변인들에게 결혼 사실을 알리고 또 공개적으로 증빙해주는 일종의 '공시'적인 성격이 강한 문서인데, 시간이 지나도 계속 그 문서를 열람할 수 있고 또 그 문서가 삭제되지 않아야 한다고 생각했다. 그렇지만서도 거기에 사비를 따로 들이는 건 아깝게 느껴졌다. 서버도 있어야 하고, 도메인도 있어야 하고...
그래서 예전부터 잘 활용해왔던 GitHub Page 기능을 사용했다. GitHub 정도면 그래도 지금부터 2~30년 정도는 너끈히 계속 서비스가 살아있을 것이라고 기대되고, GitHub Page가 당분간은 계속 쭉 무료로 운영될 것으로 기대가 되니, 별 걱정없이 사용해도 되겠다 싶었다. 관련 소스도 같이 관리해주고, 도메인도 나름 이쁘게 만들어주는 것도 마음에 들었고.
GitHub Page의 기본적인 사용법은 여기서 다루지 않는다.
처음 개발할 때에는 GitHub Page로 서빙될 수 있도록, 빌드 > 푸시 등을 해주는 간단한 Shell 스크립트를 작성해서 직접 실행했다.
# gh-deploy.sh
#!/usr/bin/env sh
# https://vitejs.dev/guide/static-deploy.html
# 정적 웹 페이지로 배포하기
# 에러가 발생될 경우 스크립트 실행을 중지
set -e
# 앱 빌드
yarn build
# 빌드된 파일이 존재하는 dist 디렉터리로 이동
cd dist
# Jekyll 처리를 우회하기 위해 .nojekyll 파일 생성
echo > .nojekyll
# CNAME 파일을 이용해 커스텀 도메인을 지정할 수도 있다.
# echo 'www.example.com' > CNAME
git init
git checkout -B main
git add -A
git commit -m 'github page deploy'
github_id=$1
github_token=$2
PAGE_URL=$(cat ../.env | grep _PAGE_URL | cut -d '=' -f2 | tr -d '"')
REPO_NAME=$(cat ../.env | grep _REPO_NAME | cut -d '=' -f2 | tr -d '"')
REPO_OWNER=$(cat ../.env | grep _REPO_OWNER | cut -d '=' -f2 | tr -d '"')
if [ ! "$github_id" ] || [ ! "$github_token" ]
then
# https://github.com/<USERNAME>/<REPO>를 remote origin으로 설정
git remote add origin https://github.com/$REPO_OWNER/$REPO_NAME.git
else
# Github Actions 에서 동작하면, Github credential을 인자로 받아 사용
git remote add origin https://$github_id:$github_token@github.com/$REPO_OWNER/$REPO_NAME.git
fi
# https://<USERNAME>.github.io/<REPO> 에 배포
git push -f origin main:gh-pages
cd ..
rm -rf dist
GitHub Page로 배포하는 코드는 위 주석에 적었다시피 Vite 공식 문서에서 알려주는 방법이다(지금은 들어가보니 Shell 스크립트에 대한 내용은 삭제되었다). 그것을 참고하여 몇몇 수정하여 사용했다.
그런데 이 방법은, 소스를 수정하고 푸시할 때마다 배번 일일이 실행해줘야 해서 번거로운 면이 있었다. 그래서 단순히 배포 브랜치에 Push만 하면 알아서 빌드와 배포가 되도록, 좀 더 편리하게 운영할 방법을 찾아봤다. 이미 GitHub Action이 활발하게 사용되고 있다는 것은 알고 있었기에 대안은 금방 나왔다.
단순 배포만 되도록 추가하는 것은 어렵지 않은데, 여기서 좀 더 커스텀한 내용은 환경 변수를 관리하는 로직이다. GitHub 저장소에 개인정보를 담을 수는 없으므로 이러한 값들은 별도의 환경 변수(.env
)로 관리하고 있었는데, 이 .env
파일은 배포시에 포함하지 않으므로 적절한 다른 방법을 제공해줘야 했다.
GitHub에서도 이러한 기능을 잘 제공하고 있다. Repository 설정에 들어가면 저장소 수준에서 환경 변수를 설정할 수 있다. 여기서 설정한 값들은 이후 GitHub Actions이 실행될 때 인스턴스에 제공된다.
[로컬 개발 환경에서 사용한 .env와 동일한 구성으로 추가해주면 된다]
이를 바탕으로 GitHub Action 스크립트를 작성하여 프로젝트 상에 추가했다. 궁극적으로는 기존에 만들어둔 Shell 스크립트를 실행하는 것이 전부이지만, 빌드할 때 이용할 .env
를 생성해주는 내용이 추가되었다(Generate .env
Step 부분). 해당 Step이 실행될 떄, Repository에 설정해둔 환경 변수들이 그대로 vars
로 제공된다. 이 값을 통하여 접근한 환경 변수 값들을 가지고서 프로젝트 루트 위치에 .env
파일을 생성해준다.
# deploy-to-gh-page.yml
name: Build and Deploy on Github Page
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
jobs:
setup-build-deploy:
runs-on: ubuntu-latest
steps:
- name: Setup Node.js 🔧
uses: actions/setup-node@v3
with:
node-version: 16
- name: Checkout to ${{ github.ref }} branch 🛎️
uses: actions/checkout@v3
- name: Generate .env ⚙️
run: |
echo "_WEDDING_DATE=\"${{ vars._WEDDING_DATE }}\"" >> .env
echo "_WEDDING_TIME=\"${{ vars._WEDDING_TIME }}\"" >> .env
...
- name: Install app 📦
run: yarn install # yarn install
- name: Setup Git Identity 🐙
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "<>"
- name: Build and Deploy 🚀
run: yarn gh-deploy ${{ github.actor }} ${{ github.token }}
위 Workflow를 프로젝트 내 .github/
디렉토리에 추가해준 뒤 Push해주면, GitHub의 Actions 메뉴에 들어갔을 때 Workflow 목록에 자동으로 등록된다. 이 Workflow를 실행하면 최종적으로, 앞서 작성했던 gh-deploy.sh
스크립트가 실행되고, gh-pages
브랜치에 빌드된 결과물이 Push되면서 실제 GitHub Page 배포가 시작된다.
위에 작성한 Workflow 이외에,
pages-build-deployment
는 GitHub Page 배포 설정에서 Deploy from a branch를 사용하면 자동으로 추가되는 Workflow이다. 여기서 설정해둔 Branch로 Push가 이루어지면, 이를 감지해서pages-build-deployment
Workflow가 시작된다.
이렇게 해두면, 그때그때 수정할 일이 생겨도 바로 코드를 고친 뒤 Push만 하면 바로 뚝딱 2,3분이면 반영이 되서 매우 편리했다. 좀 더 급한 경우에는, GitHub 웹에서 직접 코드를 수정하고, Workflow를 수동 실행해도 바로 빌드가 가능하니 좋았다. 다만 작성한 Workflow 스크립트(.yaml
)가 올바르게 작성된건지 테스트하는 방법을 알 수가 없어, 직접 빌드를 돌려보기 전까지는 모르다보니 시행착오의 시간이 좀 걸렸다. 공식 문서도 나름 자세하게 적혀있기는 하지만 직관적으로 읽히지는 않았다.
그다지 복잡한 기능이 들어간 페이지가 아니다보니, 사용된 기술 스택도 그렇게 특별할 것은 없었다.
회사에서는 항상 Vue와 Nuxt를 사용하고 있다보니, 평소 잘 쓰지 않는 라이브러리를 쓰고 싶었다. TypeScript도 마찬가지. 다음에는 기회가 된다면, 요새 핫해진 Svelte나 Astro를 써보는 것도 재미있을 듯.
Sass를 썼다는 것은 특별할 것이 전혀 없지만, 그보다는 UI 프레임워크를 쓰지 않고 UI를 최대한 직접 만들고자 했다. 회사에서는 퍼블리싱을 직접 하지 않기에, 큰 틀의 구조부터 세세한 부분까지 직접 만드는 것을 해보고 싶다는 생각이 컸다. 또 디자인이나 기능 등이 급히 추가되거나 수정되어야 할 경우들을 고려하여 변경이 용이하게 구조를 잡아보는 연습도 해보고 싶었다.
결과적으로 만족스러운 경험이었다. 결혼식 전까지도 은근히 기능을 수정할 일이 많았는데, 처음 구조를 잘 잡았던 터라, 이미 청첩장이 지인들에게 공유되기 시작한 후에도 빠르게 대응할 수 있었다.
Web FE 프로젝트를 바닥부터 처음 만든 적이 최근에 없었기에, 가장 마지막으로 환경 구성을 했던 것도 Webpack 정도에 머물러 있었다. 그래서 다양한 기술들이 속속 등장하고 있는 것을 뉴스로만 접하고 직접 써보지 못한 것이 늘 아쉬움으로 남았다. 그래서 이번 기회에 요새 많이들 사용한다는 Vite를 빌드 도구로 처음부터 사용해봤다. 프로젝트 크기가 작다보니 그렇게 다른 점은 잘 모르겠지만, Rollup 기반의 빌드 환경 구성을 처음으로 해봤다는 것의 의미를 두자.
직접 만들지 않은 것 외에 주요 기능 구현에 사용한 외부 라이브러리
계좌 안내, 카카오톡 링크 공유 등의 기능에서 사용자 동작에 대한 알림을 표시할 때 사용했다. 워낙 유명한 라이브러리이니 설명은 생략.
달력 기능과 D-day 기능을 만들 때 dayjs를 썼는데, 달력을 만들 때 주차 별 날짜 데이터를 뽑아주는 아주 유용한 플러그인. 플러그인 개발자가 중국인인 듯 한데, 구현하다가 관련 버그를 발견해서 이를 제보하는 진귀한 경험을 했다. 다행히 개발자가 활발한 오픈소스 개발자이어서, 제보한 다음날에 바로 수정해줬다.
청첩장에 들어갈 내용 중 아내와 가장 첫번째로 정한 것은 랜딩 화면에 대한 것이었다. 견본으로 미리 받은 웨딩 사진이 마음에 들었던 것을 계기로 청첩장을 직접 만들기로 결정했던 만큼, 청첩장을 켜자마자 나오는 화면은 가장 공들여서 만들었던 것 중 하나였다. 디자인적인 것 외에, 개발 측면에서 가장 고민하였던 것은 반응형에 대한 처리였다.
모바일 청첩장이니만큼 대부분의 경우 모바일 환경에서 청첩장을 보게 될 테니, 모바일에서 최적화되어 보여질 수 있는 사진을 골라야 했다. 다행히 우리가 가장 마음에 들었던 사진은 세로로 찍은 사진이었고, 반응형으로 대응하도록 스타일을 부여하여도 대부분의 모바일 기기의 Portrait 방향 비율에 들어맞았다. 사진의 위/아래를 약간 크롭하는 정도로 만족스러운 결과가 나왔다.
[iPhone SE 기준 화면 비율(좌측)과 iPhone 12 Pro 기준 화면 비율(우측)]
다만 추가로, PC에서 접속하더라도 괜찮은 모습의 화면이 나오길 바라는 나의 요구사항을 함께 충족해야 했다. 그런데 Portrait 방향에 맞추어 단순히 사진 크기를 늘이는 것으로는, 처음의 스타일 대로 구현하면 대부분 화면이 어색하게 나왔다. 화면의 가로 크기가 늘어남에 따라 사진이 확대되는데, 그렇게 하면 가로 크기가 일정 수준 이상 커졌을 때, 인물의 얼굴이 화면 윗쪽에 너무 붙어버렸다.
그래서 일정 수준 이상으로 가로 크기가 커지면, 이에 대응하여 사진이 아래로 조금 내려가도록 했다.
@media screen and (min-width: 1024px) {
$padding: calc((100vw - 1024px) * 0.3);
background-position-y: calc(50% + $padding);
.guide {
opacity: 0.8;
}
}
또, 얼굴 아래의 상반신이 많이 가려지면서 답답하지 않도록, 하단 안내 영역을 반투명하게 처리했다. 이렇게 하니 제법 자연스러워졌다.
[PC 브라우저에서 확인했을 때 화면 비율]
예식 일시를 달력과 함께 보여주고 싶다는 기획자(아내)의 강한 요청이 있어, 달력을 구현하였는데 이때 위에 언급한 dayjs-plugin-calendar-sets 플러그인을 아주 유용하게 사용했다.
// main.tsx
...
import dayjs from 'dayjs';
import CalandarSets from 'dayjs-plugin-calendar-sets';
dayjs.extend(CalandarSets);
dayjs
는 기본적인 기능은 충실히 제공하지만, 날짜 데이터를 다루는 기능을 다양하게 제공하지는 않는다. 그래서 위처럼 dayjs
전역 객체에 내장된 .extend()
를 호출하여 플러그인을 등록하면, dayjs
의 기능이 확장된다. 이제 dayjs
에서 플러그인이 제공하는 기능을 사용할 수 있다.
위 플러그인을 사용하면, 특정 연/월/일을 기준으로 해당 월의 1주일 단위 날짜 데이터를 배열로 만들어줄 수 있다. 달력 기능과 같이, 매주 단위로 날짜를 보여줘야 하는 요구사항에 딱 필요한 기능이다.
type DayjsWeek = [string, string, string, string, string, string, string];
export type DayjsMonth = DayjsWeek[];
export const getCalandarDataset = (date: Dayjs) => {
// date가 전달한, 특정 연도 및 월에 대한 주차 별 날짜 데이터
const sets = dayjs
.calendarSets({
year: date.year()
})
.month({ month: date.month() }) as DayjsMonth;
const weeks = sets.map(
(week) =>
// dayjs 데이터에서 날짜 값만 추출
week.map((day) => (day === '' ? '' : `${dayjs(day).date()}`)) as DayjsWeek
);
return weeks;
};
그런데 이 플러그인에는 한가지 우리의 요구사항과 맞지 않는 것이 있었다. 이 플러그인이 만들어준 데이터는 주차 별 첫번째 요일이 월요일로 되어있었다. 하지만 한국의 달력은 일반적으로 한 주의 시작이 일요일이다.
나도 처음 알았는데, 중화권의 달력은 한 주의 시작이 월요일부터인 듯 하다(관련 설명).
이 플러그인 개발자가 중국인이다보니, 개발자 입장에서는 자연스럽게 한 주의 첫날이 월요일인 것을 기준으로 데이터가 생성되도록 플러그인을 구현한 것이다. 한국 사람을 대상으로 하는 달력에서는 이렇게 표기할 수 없었기에 플러그인을 그대로 쓰지 않고 약간 수정을 해서 써야만 했다.
/**
* 달력 데이터의 매주 첫번째 요일을 월요일에서 일요일로 변경
* @param weeks 첫번째 요일이 월요일인 dayjs CalandarSets 기반 데이터
* @returns 첫번째 요일이 일요일인 dayjs CalandarSets 기반 데이터
*/
const convertCalandarData = (weeks: DayjsMonth) => {
weeks.push(['', '', '', '', '', '', '']);
weeks[0].unshift('');
for (let i = 0; i < weeks.length - 1; i++) {
const sunday = weeks[i].pop();
weeks[i + 1].unshift(sunday!);
}
weeks[weeks.length - 1].pop();
return weeks.filter((week) => !week.every((date) => date === ''));
};
export const getCalandarDataset = (date: Dayjs) => {
// date가 전달한, 특정 연도 및 월에 대한 주차 별 날짜 데이터
...
const weeks = convertCalandarData(sets).map(
(week) =>
// dayjs 데이터에서 날짜 값만 추출
week.map((day) => (day === '' ? '' : `${dayjs(day).date()}`)) as DayjsWeek
);
return weeks;
};
앞서 언급한, 플러그인의 오류를 발견해 제보하게 된 것도 지금 돌이켜보면 참으로 우연이었고 신기했다. 오류 현상은 아래와 같았다.
.month()
를 호출시, 인자로 무엇을 전달하든 데이터가 28일까지만 정상적으로 출력됨그런데 이 청첩장을 1월부터 본격적으로 만들기 시작했는데, 1월까지는 이런 이슈가 없다가 2월이 되고 나니 갑자기 이슈가 생겨난 것이다. 아무리 생각해도 2월이 28일까지만 있다는 것 외에는 특별히 다를 것이 없어, 이슈로 등록했다. 하마터면 기능을 직접 다시 구현해야 할 수도 있었는데, 다행히 개발자가 바로 확인하고 반영해줘서 큰 문제 없이 넘어갔다.
달력과 더불어, 기획자(아내)의 강한 요청 중 하나가 이 D-day 카운터였다. 지금 돌이켜보면 이 기능은 오히려 결혼식장에 오시는 손님들보다 나와 아내가 더 자주 썼던 기능같다. 그때그때 청첩장에 들어가 카운터를 보면, 시간/분/초까지 나오니만큼 좀 더 박진감 넘치게 결혼이 얼마나 남았는지 느낄 수 있었다.
단순 카운터 기능 외에 좀 더 특징을 부여한 기능은 아래와 같았다.
개인적으로는, 청첩장을 두고두고 보고자 하려는 나의 의도를 고려할 때 D+Day 카운터의 기능이 좀 더 중요했다.
단순 지도 이미지를 제공하는 것으로 충분했겠지만, 이번 기회에 공개된 지도 기능을 연동하여 달아보고 싶었다. 그리고 지도 연동하는 것은 의외로 꽤 까다로웠고 해야할 일이 많았다.
단순히 기능만 동작하게 하는 것에서 그쳤으면 쉽게 끝났겠으나, React를 쓰니만큼 Hook 형태로 깔끔하게 독립시켜 코드를 짜겠다는 욕심 때문에 불필요하게 시간을 좀 소모했다.
// utils/hooks/useKakaoSDK.ts
import { useState, useEffect } from 'react';
import {
KAKAO_CLIENT_ID,
KAKAO_INTEGRITY_VALUE,
KAKAO_SDK_VERSION
} from '@/configs/constants';
const KAKAO_SDK_SRC = `https://t1.kakaocdn.net/kakao_js_sdk/${KAKAO_SDK_VERSION}/kakao.min.js`;
type KakaoSDK = typeof Kakao;
function useKakaoSDK() {
const [kakaoSDK, setKakaoSDK] = useState<KakaoSDK>();
useEffect(() => {
const kakaoSDKURL = `${KAKAO_SDK_SRC}`;
let script: HTMLScriptElement | null = document.querySelector(
`script[src="${kakaoSDKURL}"]`
);
if (!script) {
script = document.createElement('script');
script.src = kakaoSDKURL;
script.type = 'text/javascript';
script.async = true;
script.integrity = `${KAKAO_INTEGRITY_VALUE}`;
script.crossOrigin = 'anonymous';
document.head.appendChild(script);
}
const loadHandler = () => {
if (!window.Kakao.isInitialized()) {
window.Kakao.init(KAKAO_CLIENT_ID);
}
setKakaoSDK(window.Kakao);
};
script.addEventListener('load', loadHandler);
return () => {
if (script) {
script.removeEventListener('load', loadHandler);
document.head.removeChild(script);
}
};
}, [kakaoSDK]);
return kakaoSDK;
}
export default useKakaoSDK;
// components/KakaoMaps.tsx
import React, { useEffect, useState } from 'react';
import useKakaoMaps from '@/utils/hooks/useKakaoMaps';
interface KakaoMapsProps {
mapId: string;
lat?: number;
lng?: number;
}
/**
* 네이버 지도
* */
type KakaoMaps = typeof kakao.maps;
function KakaoMaps(props: KakaoMapsProps) {
const { mapId, lat = 33.450701, lng = 126.570667 } = props;
const kakaoMaps = useKakaoMaps();
const [mapInstance, setMapInstance] = useState<kakao.maps.Map | null>(null);
// 지도 생성
useEffect(() => {
if (!kakaoMaps) return;
const mapElement = document.getElementById(mapId)!;
// 파일에서 스타일 부여시 지도 기능 오동작
mapElement.style.width = '100%';
mapElement.style.height = '450px';
const mapOptions: kakao.maps.MapOptions = {
center: new kakaoMaps.LatLng(lat, lng),
level: 4
};
const map = new kakaoMaps.Map(mapElement, mapOptions);
setMapInstance(map);
}, [mapId, kakaoMaps, lat, lng]);
// 지도 상에 UI 표시
useEffect(() => {
if (!(kakaoMaps && mapInstance)) return;
// 지도 컨트롤
const zoomControl = new kakaoMaps.ZoomControl();
mapInstance.addControl(zoomControl, kakaoMaps.ControlPosition.RIGHT);
// 마커
const marker = new kakaoMaps.Marker({
position: new kakaoMaps.LatLng(lat, lng)
});
marker.setMap(mapInstance);
}, [kakaoMaps, mapInstance, lat, lng]);
return (
<>
{!kakaoMaps ? (
<div className="map-empty">
<p className="title"><지도...이었던 것></p>
<p>지도를 표시하지 못했어요. 가끔은 이럴 때도 있지요.</p>
<p>제대로 안 만든 신랑에게 어서 알려주세요!</p>
</div>
) : (
<div id={mapId} className="map-enabled" />
)}
</>
);
}
export default KakaoMaps;
그리고 무엇보다도, 처음에는 네이버 지도를 연동했는데 어떤 이유에선지 퍼포먼스가 굉장히 좋지 않아 기껏 만든 기능을 폐기했다. 똑같이 네이버 지도를 쓰는 네이버의 공식 예제나, 다른 사람들이 만든 서비스에서는 버벅거림 없이 잘 동작하는 것으로 보아 내 쪽 코드에 뭔가 문제가 있었던 것일 텐데. 디버깅을 해볼까 했지만 시간이 그리 많지 않아 포기했다. 그래도 처음 만들면서 겪었던 시행착오를 토대로 카카오 지도는 금방 연동했으니, 오히려 좋았던 걸로 하자.
또 하나의 공들여 만든 기능인 갤러리. 이렇게 아코디언 UI를 제공해주는 라이브러리는 많이 있으나 한번 정도는 직접 만들어보고 싶었다.
UI 자체는 그렇게 특별할 것이 없는데, 갤러리에 표시할 이미지도 GitHub Page를 통해 함께 서빙해야 한다는 제약조건이 있었다보니 이를 고려하여 코드 구조를 잡았다. Vite에서 제공해주는 기능을 최대한 수정 없이 이용하면서, 코드 복잡도는 최소화하는 방향으로 구현했다. CSS에 작성된 url()
에 들어있는 경로를 Vite가 자동으로 해석해서 에셋을 로드하고 제공해주는 점을 최대한 활용했고, for-loop을 단순 순회하는 것으로 갤러리 목록을 채울 수 있도록 파일 구조를 잡았다.
/
┣ 📂src/
┃ ┣ 📂public/
┃ ┃ ┣ 📂images/
┃ ┃ ┃ ┣ 📂gallery/
┃ ┃ ┃ ┃ ┣ 📂1/
┃ ┃ ┃ ┃ ┃ ┣ 📜image.jpg
┃ ┃ ┃ ┃ ┃ ┗ 📜thumb.jpg
┃ ┃ ┃ ┃ ┣ 📂2/
┃ ┃ ┃ ┃ ┃ ┣ 📜image.jpg
┃ ┃ ┃ ┃ ┃ ┗ 📜thumb.jpg
...
물론 이런 식으로 에셋을 다루면 하면 확장성은 최악이다. 개별 이미지 단위로 폴더를 일일이 생성해줘야 하고, 이미지의 교체나 순서 변경 등을 하려면 매번 새로 배포해야 한다. 하지만 무료로 호스팅받아 서비스를 제공하는 상황인데, 당연히 이정도는 맞춰줄 수 있다.
그리고, Pagination이나 Lazy load도 고려해서 만들었다면 좀 더 의미가 있었겠지만, 고작 9장의 사진을 위해 그렇게까지 할 필요는 없었다보니 단순히 UI를 만드는 정도에서 그쳤던 것이 아쉽다. 사실, 결혼식이 임박해오면서 더 많은 사진을 갤러리에 추가하자는 기획자(아내)의 의견이 있었으니 이래저래 정신이 없는 와중에 추진하지 못했다.
아, 스와이프 기능은 넣는다고 한게 깜빡하고 못했다.
Vite 설정을 잘 수정하면, 빌드된 SPA Web의 메타 정보는 간단하게 추가할 수 있다. 그래서 카카오톡을 통해 URL을 공유하면 꽤 그럴 듯한 썸네일과 설명이 노출되도록 할 수 있고, 이거면 충분하다고 처음에 생각했다. 그럼에도 불구하고 카카오톡 공유하기 기능의 도입을 기획자(아내)가 강하게 주장했는데 그 논리는 간단했다.
"모바일 청첩장 눌렀다가 나도 모르는 대출이…" 신종 보이스피싱 주의보
(예식장에 주로 오시게 될)어르신들이 모르는 주소로 모바일 청첩장 링크를 전달받으면 피싱 사기일까 걱정되셔서 잘 들어가지 않으신다.
위 링크는 무려 2023년 7월, 우리가 결혼을 한 이후에 작성된 기사이니, 그때나 그 전이나 지금이나 이러한 피싱 사기가 성행하고 있었던 모양이다. 당시에도 충분히 그럴 수 있겠다는 생각이 들어 바로 카카오톡 공유하기 기능을 개발하여 추가했다.
카카오톡 개발자 문서가 꽤 자세하다보니 몇번 읽으면 기능 개발은 금방 끝난다. 다만 작업하면서 알아두면 좋을 몇가지를 적어두면,
import { useState, useEffect } from 'react';
import {
KAKAO_CLIENT_ID,
KAKAO_INTEGRITY_VALUE,
KAKAO_SDK_VERSION
} from '@/configs/constants';
const KAKAO_SDK_SRC = `https://t1.kakaocdn.net/kakao_js_sdk/${KAKAO_SDK_VERSION}/kakao.min.js`;
type KakaoSDK = typeof Kakao;
function useKakaoSDK() {
const [kakaoSDK, setKakaoSDK] = useState<KakaoSDK>();
useEffect(() => {
const kakaoSDKURL = `${KAKAO_SDK_SRC}`;
let script: HTMLScriptElement | null = document.querySelector(
`script[src="${kakaoSDKURL}"]`
);
if (!script) {
script = document.createElement('script');
script.src = kakaoSDKURL;
script.type = 'text/javascript';
script.async = true;
script.integrity = `${KAKAO_INTEGRITY_VALUE}`;
script.crossOrigin = 'anonymous';
document.head.appendChild(script);
}
const loadHandler = () => {
if (!window.Kakao.isInitialized()) {
window.Kakao.init(KAKAO_CLIENT_ID);
}
setKakaoSDK(window.Kakao);
};
script.addEventListener('load', loadHandler);
return () => {
if (script) {
script.removeEventListener('load', loadHandler);
document.head.removeChild(script);
}
};
}, [kakaoSDK]);
return kakaoSDK;
}
export default useKakaoSDK;
카카오 지도와 카카오톡 공유하기 기능을 제외하면, 그 밖의 외부 서비스는 단순 링크만 걸어두면 모바일 앱이 켜지도록 알아서 처리해주니 연동이 편리했다.
개발을 진행하면서 일감 관리를 GitHub Issues를 통해서 관리했었는데, 대부분의 것들은 생각한 대로 구현하였지만 못한 것들도 일부 있었다. 대부분은 충분한 검토를 못해 구현하지 못했거나, 아이디어 수준에서 머무르고 만들지 못한 것들이다.
청첩장을 만들면서 추구했던 것이 철저한 0-Cost 였어서, 최소한의 서버조차도 사용하지 않는 것을 목표로 했었다보니 가장 후순위로 밀렸었다. 단순 Client 로직만으로는 댓글 기능을 넣기가 사실상 불가능한 것으로 알고 있어서, 어떻게든 가능한 방법이 있을런지 생각만 해보다 결국 만들지 못했다. Notion API나 Google Spreadsheet를 쓰면 되었을 듯 하지만, CORS를 서버 없이 해소하는 것이 없는 것으로 알고 있어 아무래도 끝끝내 못 만들었을 듯 하다. 물론 utterences와 같은 방식도 있지만, 이 기능은 GitHub 로그인이 필수였기에 그다지 내키지 않았다.
GitHub Page로 서빙하는 SPA Web에 댓글 기능을 탑재할 수 있는 좋은 방법을 아신다면, 댓글로 좋은 정보 꼭 알려주세요!
나도 이번에 예식 준비하면서 처음 알았는데, 참석 여부를 초대자에게 알려주는 기능이라고 한다. 주로 스몰 웨딩을 하거나 별도의 식당을 빌려 피로연을 진행하는 경우 참석자 조사가 중요하다보니 이 런 기능이 필요한 경우도 있다고 한다. 내 기억에도, 한 식당을 빌려 스몰 웨딩으로 진행되었던 결혼식에 초대된 적이 있었는데, 주최자가 참석 여부를 여러 차례 확실하게 물어보고 그랬던 적이 있다.
이 기능도, 위에 댓글 기능과 비슷한 이유로 아마 구현이 어려웠을 것으로 보인다. 우리는 일반적인 형태의 예식을 준비했기에 이 기능이 그렇게 중요하지도 않았고.
대부분의 기능 제안과 디자인 리소스는 아내가 결정하고 제작했다. 사실상 외주 프로젝트를 진행하는 느낌이었는데(물론 내가 乙), 매주 한번 정도 만나서 산출물을 함께 리뷰하고, 그 자리에서 수정하거나 또는 아이데이션을 하는 시간을 2,3주 정도 가졌었다.
앞서 몇번 언급했던, 견본으로 받은 촬영 사진 중 아내에게 가장 마음에 들었던, 현재의 메인 사진을 토대로 전체적인 컬러와 톤을 결정하고, 이를 토대로 디자인 방향도 정했다. 촬영했던 스튜디오가 전반적으로 파스텔 톤의 분위기를 잡아줬는데 이것이 우리 둘의 마음에 쏙 들어서, 청첩장도 그런 방향으로 컨셉을 잡았다.
한때 홍보마케팅 업무를 했었던 아내의 디자인 센스가 이렇게 빛을 발했다. 종이 청첩장도 직접 디자인하고 출력까지 맡겼다. 내 아내 금손.
[함께 직접 만들었다고 생각하니 더 애착이 갔다]
시간과 작업량만을 순수하게 따졌을 때 그렇게 큰 리소스가 필요한 프로제트가 아니었음에도 불구하고, 돌아보면 은근히 신경쓸 일이 많았고 제법 공수가 투입되었다. 결혼을 해본 분들은 익히 아시겠지만, 결혼을 하기까지 꽤 다양한 것들을 신부와 함께 준비해야 하는데 그 물리적 / 정신적 리소스들 가운데 짬을 낸다는 것이 그리 쉽지는 않았다.
그럼에도 이렇게 만들어놓고 나니, 문득문득 들어가보면 예식할 때 기억도 새록새록 나니 좋고, 이제는 혼자가 아닌 유부라는 사실을 곱씹게 되는 효과도 있으니 제 역할을 톡톡히 하는 듯 하다.
혹시나 청첩장을 직접 만들까 고민하고 계시다면, 약간의 시간과 리소스를 들여 도전해보시길. 분명 좋은 추억으로 남을 것이다.