로토의 블로그

내가 온라인 청첩장을 만든 방법

내가 온라인 청첩장을 만든 방법 대표 이미지

2022년 10월 22일, 결혼이라는 인생의 매우 큰 행사를 앞두고 있다.

웹 개발로 밥벌이를 하는 사람으로써, 온라인 청첩장 사이트는 직접 만들어야 하는거 아닌가..라는 생각을 항상 가지고 있었고 온라인 청첩장을 어떠한 과정과 의사결정 과정을 거쳐서 만들었는지를 기록해보았다.

도메인 구입하기

온라인 청첩장을 만들겠다고 결심하고 가장 먼저 한 일은 도메인 구입이다. rotojuna.wedding 이라는 도메인을 올초에 구입했다.

wedding 도메인은 godaddy에서 구입할 수 있다.

시작이 반이라고 했다. 벌써 반은 한 거 같았다.

개발 계획하기

이제 구현만 하면 된다. 구현에 앞서서 어떤 방식으로 만들지를 생각해봤다.

플랜A - RETRO RPG 웹게임 만들기

초창기의 계획은 phaser.js 등을 이용한 레트로 RPG를 만들려고 했었다. 필드를 돌아다니면서 NPC들을 조사하면 결혼식에 대한 정보를 알려주는 방식을 생각했다.

이렇게 할 경우 재미는 있지만 일단 정보를 얻는 것 자체가 직관적이지 않고, 또 모바일에서는 가상 패드를 띄워서 조작할 수 있게 해야하는데 공수대비 별로 임팩트가 있을 거 같지 않아서 기각했다.

플랜B - RPG 메이커 툴 이용해서 웹 게임 만들기

rpg maker 95

내가 최초로 코딩 감각을 익히는데 큰 도움을 준 툴이 RPG쯔꾸르95를 가지고 놀면서인데, 마침 그때의 추억도 되살릴겸 최신 버전의 RPG 메이커 툴을 이용하는 방법도 고민해봤다. 심지어 요즘 버전은 웹 버전으로도 빌드가 되는 것 같았다.

아래 이미지는 최신 버전.

rpgmaker

다만 이걸 선택해도 내가 원하는 느낌의 리소스 셋을 구하기는 힘들 것 같아서 포기했다.

플랜C - RETRO RPG 느낌의 웹 사이트로 만들기

nes.css

패미컴을 즐겨하던 세대였던지라 예전부터 nes.css를 이용한 사이트를 만들고 싶다는 생각을 꾸준히 해왔었고 이걸 이용해서 청첩장 사이트를 고전 롤플레잉 게임의 홍보물처럼 만들어보면 재밌겠다 싶어서 이 플랜으로 최종 결정했다.

nes.css 말고로 Retro Game Style CSS 툴은 여러가지가 있는데, 관심있는 사람은 https://codeburst.io/10-amazing-and-retro-css-kits-24612169f550 이 게시물을 참고하면 좋을 것이다.

픽셀아트 이미지 외주 찾기

청첩장 구현방안을 Retro Game Style 사이트로 정했으니 다음으로는 사이트에 메인 이미지로 쓸 이미지를 고민해볼 차례였다.

TMI이지만 온라인 활동명을 로토로 썼던 만큼 드래곤퀘스트 시리즈를 상당히 좋아하는데, 그중에서도 제일 좋아하는 3가 리메이크 된다는 소식에 첨부되어 있던 이미지가 마음을 끌었다.

dragon quest 3 remake

위의 이미지를 패러디해서 쓰면 좋을 것 같아 크몽에서 픽셀아트를 전문으로 하는 전문가분을 찾아 해당 이미지의 느낌으로 만들어달라고 요청했다.

컨셉 요청용 이미지

아래 이미지는 그렇게해서 나온 이미지이다.

픽셀아트 외주 결과

폰트 선택

많은 Software Engineer분들이 애용하고 있을 눈누에서 폰트를 찾아봤다.

아무래도 고전 게임의 이미지다보니 폰트도 옛날 OS에서 쓰던 느낌의 폰트를 찾았고, 몇가지 적절한 폰트들이 꽤 있었다.

마비노기를 재밌게 했던 추억이 있어 마비노기 폰트로 할까도 했었고

마비노기 폰트

아래와 같은 폰트들도 나름 매력적이었다.

갈무리9 폰트

hbios

최종적으로 선택한 폰트는 둥근모체였다.

둥근모체

둥근모체 공식 홈페이지

로고 이미지

처음에는 배경 이미지만 깔았지만, 반려인이 종이 청첩장 작업을 직접 하게 되면서 드래곤퀘스트 로고를 패러디한 청첩장 로고를 만들어주었다.

청첩장 로고 이미지

구입해놓고 거의 안 쓰고 있던 어도비 포토샵을 이용해, 배경 이미지와 로고 이미지를 적절히 합성하여 청첩장의 메인 이미지로 사용하게 되었다.

청첩장 메인 이미지

tailwind

최근 next.js 기반 작업은 거의 대부분 chakra-ui와 함께 했는데, 개인적으로는 chakra-ui 방식으로 스타일링을 하는 게 제일 편했기 때문이다.

astro.js에서도 chakra-ui를 쓸 방법을 궁리해볼까 하다가, 사용법은 다르지만 이번 기회에 tailwind를 써보자! 싶어서 tailwind를 썼다.

tailwind 연동은 상당히 쉽게 가능하다. 공식문서

이제 대충 정해졌으니..코딩 시작!

처음에 도메인을 사두었을 때는 next.js로 만들 것 같아서 Github 프로젝트 세팅만 해두고, TBD라느 글자만 대문짝만하게 나오게 해둔 상태였다.

TBD

청첩장 사이트는 정적인 부분이 대부분일 것이기 때문에, 최근 애용하고 있는 astro.js를 사용하기로 했다. 이 글을 실어나르고 있는 블로그도 astro.js로 되어있기도 하고. 관련링크

정적인 부분과 동적인 부분을 분리하기

astro.js의 특징 중 하나인 Islands Architecture를 이용해서 구현하면, 사이트의 정적인 부분은 빌드 시 HTML/CSS로만 구성되어 최초 로딩 시 불러와야하는 JS 번들의 사이즈를 최소화 시킬 수 있다. UI 인터랙션이 필요한 부분만 react, vue.js, svelte 등을 이용해 구현하고, 여러가지 지연로딩 기법을 이용해 최적화 할 수 있다.

우선 정적인 부분을 생각해보자.

  • 인사말
  • 혼주 / 신랑신부 정보
  • 송금 버튼
  • 네비게이션 호출 버튼
  • 찾아오시는 길

…사실 필요한 필수 정보들은 다 정적인 부분이라 볼 수 있다.

그렇다면 동적인 부분을 넣는다면 무엇이 있을까?

  • 방명록
  • 그외 예상치 못한 동작들

사실상 거의 정적인 부분들이다.

기본 레이아웃 컴포넌트들 잡기

현재 청첩장 사이트는 아래의 단락으로 구분되어 있다.

  • 인사말
  • 혼주 / 신랑신부 정보
  • 찾아오시는 길
  • 송금 정보
  • 이벤트
  • 방명록

각 단락은 RetroMessageBox 라는 컴포넌트를 만들어서 썼다.

astro.js에서는 아래처럼 컴포넌트를 선언해서 쓴다.

위의 ---로 JS 코드를 넣는 부분이 묘하게 PHP와 닮았다.

---
const { title } = Astro.props
---

<div class="nes-container is-dark is-rounded m-0">
  {title && <h2 class="mb-4 text-2xl md:text-3xl">{title}</h2>}
  <div class="mt-8">
    <slot />
  </div>
</div>

<slot />이 강력한 부분인데, 기존 react 컴포넌트처럼 children props 처럼 쓸 수도 있고 그외에 render props으로 활용했던 부분들을 대체할 수 있다. leftRender, rightRender 처럼 썼던 그런 방식들 말이다. 딴 이야기지만 vue.js 에서도 slot을 지원했던 것 같다.

slot에 대해서는 이 문서를 참고하도록 하자.

네비게이션 버튼 누르면 앱 실행되게 하기

네비게이션 버튼들

네비게이션 버튼을 눌렀을 때 해당 앱이 바로 실행이 되면서 길안내를 시켜주면 상당히 편리할 것이다. 요러한 기능은 모바일 환경에 한해서 구현이 가능하다.

우리가 웹브라우저 내에서 https://로 시작하는 주소로 통신하듯이, 각 앱별로 정의된 scheme이 있고 이를 통해 모바일 디바이스 상의 브라우저에서 앱을 호출할 수 있다. 티맵과 네이버 지도의 경우 모바일 웹 내에서 앱을 호출하기 위한 scheme이 검색해보면 쉽게 나와서 쉽게 적용했다.

가령 티맵의 경우 a 태그에 아래의 링크를 href로 걸면, 티맵 앱이 실행 되면서 길안내를 쉽게 받을 수 있다.

<a class="flex-1" href="tmap://search?name=더빅토리아웨딩파티">
  <button class="w-full nes-btn text-ellipsis">티맵</button>
</a>

tmap://search에 querystring으로 name을 넘기면, 해당 버튼을 누르면 티맵 앱이 실행되면서 name으로 넘긴 값으로 검색된 결과가 바로 나온다.

네이버지도도 마찬가지. query keyword를 encodeURIComponent 처리하는 것과 appname 파라메터를 넣는 부분만 조금 다르다.

<a class="flex-1" href="nmap://search?query=%EB%8D%94%EB%B9%85%ED%86%A0%EB%A6%AC%EC%95%84%EC%9B%A8%EB%94%A9%ED%8C%8C%ED%8B%B0&amp;appname=https://rotojuna.wedding">
  <button class="w-full nes-btn text-ellipsis">네이버지도</button>
</a>

그래서 RetroNaviButton 이라는 컴포넌트를 만들어 처리했다.

---
import { weddingHallName } from 'constants/constants'
const { title, scheme } = Astro.props
---

<a class="flex-1" href={`${scheme}`}>
  <button class="w-full nes-btn text-ellipsis">{title}</button>
</a>

RetroNaviButton 컴포넌트는 아래처럼 불러다 썼다.

<RetroNaviButton
  title="티맵"
  scheme={`tmap://search?name=${weddingHallName}`}
/>
 <RetroNaviButton
  title="네이버지도"
  scheme={`nmap://search?query=${encodeURIComponent(weddingHallName)}&appname=https://rotojuna.wedding`}
/>

문제가 되는 건 카카오내비였는데, 카카오내비도 위처럼 scheme이 있을 거 같지만 공식 문서에서는 kakao sdk를 통해서 호출하도록 되어있다. API 키도 발급 받아야 한다.

// SDK 초기화
window.Kakao.init(apiKey)

// SDK를 통한 호출
window.Kakao.Navi.start({
  name: '더빅토리아웨딩파티',
  x: lng,
  y: lat,
  coordType: 'wgs84',
})

그렇기 때문에 네비게이션 버튼들중 이 부분만 react로 구현이 되어있다.

한 가지 특이한 점은 티맵과 네이버지도의 경우 scheme을 통해 실행하면 검색결과 목록을 보여주지만, 카카오네비는 아예 길안내 시작까지 해버린다는게 다른 점이라고 할 수 있겠다.

카카오내비 관련 내용은 이곳을 참고.

송금 링크 만들기

송금의 경우 카카오페이와 토스뱅크의 송금을 적용했다.

카카오페이

카카오페이의 경우 손쉽게 송금을 받기 위한 QR코드를 만들 수 있다. 이 QR코드를 폰으로 찍으면, 카카오페이 송금이 바로 실행이 되면서 해당 사용자에게 간편하게 송금을 시킬 수 있는 구조이다.

이 송금QR코드는 사실 송금 페이지 랜딩용 URL이어서 QR코드 이미지 형태 외에도 URL 형태로도 공유할 수 있다. 이 송금 링크에 접속하면 위에서 네비게이션 호출에서 이야기한 scheme을 호출해서 송금 화면을 띄우는 식이다.

송금 링크 페이지의 페이지 코드를 까보면 쉽게 이해할 수 있다.

페이지 로딩 시 송금 화면을 띄우기 위한 scheme을 JavaScript를 이용해 호출하며, 자동이동이 안 될시를 대비한 링크도 제공하고 있다.

<script>
(function($){

    var redirect = function() {
        var web2appOptions = {
            urlScheme: 'kakaotalk://kakaopay/money/to/qr?qr_code=281006011000055439228587',
            appName: '카카오톡',
            intentURI: 'intent://kakaopay/money/to/qr?qr_code=281006011000055439228587#Intent;scheme=kakaotalk;package=com.kakao.talk;end;',
            storeURL: 'itms-apps://itunes.apple.com/app/id362057947',
            willInvokeApp: function () {
            },
            onUnsupportedEnvironment: function () {
                $("#move_mobile").hide();
                $("#move_err").show();
            }
        };
        daumtools.web2app(web2appOptions);
    };

    $("#forceRedirect").click(function(e){
        e.preventDefault();
        redirect();
    });

    if (daumtools.userAgent().platform === 'pc') {
        $("#move_mobile").hide();
        $("#move_pc").show();
    } else {
        setTimeout(redirect, 500);
    }
})(jQuery);
</script>

아래는 링크용 버튼.

<div id="move_mobile" class="article_transfer">
    <img src="//t1.daumcdn.net/kakaopay/20170106140351/img_transfer.gif" alt="" class="img_character" />
    <h2 class="tit_bridge">카카오페이로 이동 중</h2>
    <p class="desc_append">이동이 지연되고 있나요?
        <a href="kakaotalk://kakaopay/money/to/qr?qr_code=281006011000055439228587" target="_blank" class="link_append" id="forceRedirect">
            <span class="txt_link">카카오페이 바로가기</span>
        </a>
    </p>
</div>

잘 보면, kakaotalk://kakaopay/money/to/qr?qr_code={QR_CODE}와 같은 부분을 발견할 수 있을 것이다.

위의 송금 페이지의 경우 당연하겠지만 PC에서는 동작하지 않는다.

카카오페이 PC 이용불가

토스뱅크

토스뱅크의 경우에도 원리는 카카오페이와 같다. 다만 카카오페이와 다른 점은, 단순 scheme 호출용 페이지가 아니라 좀 더 풍부한 기능을 제공하는 페이지라는 점이다.

토스뱅크 송금 랜딩 페이지

사기계좌 여부도 볼 수 있고, 몇명이 다녀갔는지도 볼 수 있다. linktree처럼 일부 링크도 걸 수 있다.

개인적으로는 토스뱅크쪽 송금 랜딩 페이지가 더 마음에 든다.

로토의 토스뱅크 랜딩 페이지 방문해보기

공유하기 기능 구현

Share API를 이용하면 모바일에서 쉽게 공유하기를 구현할 수 있다.

해당 기능의 경우 UI인터랙션에 해당하기 때문에, react 컴포넌트로 구현했다.

Share API를 지원하지 않는 환경의 경우, 그냥 클립보드에 복사되게 했다. 클립보드의 경우 Clipboard.writeText를 이용해서 처리하면 될 것 같았다.

하지만 의외의 난관이 있었는데, 카카오톡 인앱브라우저 환경에서는 Share API 동작도 안 되고 Clipboard.writeText는 권한 때문에 동작하지 않는다는 점이다. 청첩장 사이트의 특성상 카카오톡과 같은 메신저를 통해 공유가 될 것이고, 해당 메신저에서 링크를 누르면 인앱브라우저로 뜰 것이기 때문에 해당 문제는 꼭 해결해야 했다.

위 문제는 고전적이고 전통적이지만 곧 Deprecated 예정인 방법으로 처리했다.

// 고전적인 클립보드 복사 방법
const $input = document.createElement('input')
$input.value = text
$input.style.position = 'absolute'
$input.style.left = '-999px'
document.body.appendChild($input)
$input.select()

document.execCommand('copy')
document.body.removeChild($input)

alert(copyMessage)

방명록 만들기

방명록 기능을 만들까 말까 하다가, 그래도 인터넷 친구들의 축하 메시지를 모아두면 그것도 나름 재밌고 기분 좋은 일이 될 것 같아 구현했다.

첫 시도. disqus 사용하기

이 블로그에도 쓰고 있는 댓글 서비스인 disqus를 써서 간단하게 붙여봤었는데, 연동도 간단하게 되고 SNS 로그인도 쉽게 되어서 이걸로 쓸까 했지만 치명적인 문제가 있었다.

바로 댓글창에 placeholder로 들어가있는 토론 시작을 바꿀 수가 없던 것이다.

disqus

DOM Loaded 된 이후에 강제로 바꿔볼까도 했는데 iframe 형식으로 불러오는거라 그것도 쉽지 않았다.

결국 자체구축의 길로 접어들었다.

자체 구축하기

데이터를 어디에 저장할지는 여러 고민이 있었는데

  • strapi 하나 띄워서 여기에 저장하기
  • next.js api 하나 파고, 여기에 파일 시스템 방식으로 넣기
  • firebase realtime database 쓰기

1번 방식의 경우 가장 익숙한 방식이지만 strapi 서버를 계속해서 띄워둬야하는 부담이 있어서 기각했고, 2번의 경우도 은근히 번거로울 것 같아 결국 3번의 방법으로 선택했다.

방명록 만든 김에.. 실시간 알람 받기

사람들이 방명록을 남길 때마다 실시간으로 알람이 받고 싶었다. 이 경우 AWS SNS 같은 걸 이용해 문자 메시지로 쏘거나 하는 방법이 있겠지만, 가장 쉬운 방법은 Slack webhook을 이용해 알람을 받는 방식일 것이다.

개인적으로 사용하는 슬랙 채널에 새 채널을 파고, 방명록이 작성될 때마다 해당 채널로 알람이 오게 했다.

이름하여 모험의 서.

모험의 서

메시지를 남겨주신 모든 분께 감사를.

어르신용 사이트 만들기

포멀 버전 메인 이미지

레트로 느낌의 사이트를 얼추 만든 후에는 포멀한 버전의 사이트를 만들었다. 오히려 이쪽이 메인 URL이고, 레트로 버전의 경우 별도의 페이지로 보냈다.

그래서 포멀한 버전의 청첩장 주소가 https://rotojuna.wedding이고, 처음에 개발한 레트로풍 청첩장의 주소는 https://rotojuna.wedding/retro이다.

뒤에 /retro 붙는게 영 마음에 안 들어서 레트로풍 청첩장 사이트의 주소를 retro.rotojuna.wedding로 바꿀까도 싶었지만, 이 경우 포멀한 버전과 레트로풍의 페이지 모두 index.astro로 바꿔야하는데 이렇게 하려면 repository를 분리해야 가능할 거 같고 두 페이지에서 공유하고 있는 컴포넌트가 있기 때문에 그러면 monorepo 형태로 만들어야 하는데 이쯤되면 작업이 산으로 가는 거 같아 그냥 현재 상태로 냅뒀다.

웨딩스냅을 제주도의 연그라미에서 찍었는데 포멀 버전에서 잘 써먹었다. 뜬금없는 소리지만 겨울에도 한 번 더 찍으러 가고 싶다.

폰트

눈누를 찾아보다 마포꽃섬이라는 폰트가 마음에 들어 이것으로 했다.

어르신용 폰트

전체적인 스타일링 다시하기

초대장

레트로 버전과 거의 모든 기능이 같고, 방명록만 없다.

레트로 버전은 다크테마가 메인이었는데 이것은 레트로풍이기 때문에 그렇고, 포멀한 버전에서는 라이트테마 기준으로 작업을 했다.

혼주측 계좌 정보 넣기

레트로 버전은 주로 친구들이나 직장동료들에게 공유되기 때문에 신랑신부의 계좌정보만 있으면 되지만, 어르신용 버전은 어르신들 네트워크 상에서 공유가 되기 때문에 혼주분들의 계좌정보를 넣어야한다고 하여 추가했다.

번외편 - 공연 티져 페이지

2022년 10월 10일에 결혼기념 펑크 공연을 앞두고 있는데, 이 티져 페이지도 astro.js로 만들었다.

party

요 페이지의 경우 청첩장 사이트보다 먼저 만들었는데, 청첩장 사이트와 공유할 컴포넌트가 없어서 아예 별도의 repository로 만들었다.

https://party.rotojuna.wedding에서 구경할 수 있다. 구경한 김에 링크를 타고 들어가서 공연예매도 부탁한다.

마치며

구현과 관련된 내용은 이정도이다. 이후에는 lighthouse 점수를 높이기 위한 몇가지 최적화 작업을 거쳤는데, 글이 제법 길어졌으므로 추가 포스트를 통해 어떻게 lighthouse 점수를 높였는지도 써보도록 하겠다.

기왕 여기까지 보신거 청첩장 사이트 구경도 하고, 방명록도 남겨주시면 좋겠다.

청첩장 코드는 후기글 이후 잘 정리해서 Github에 오픈하도록 하겠다.