두 번째 홈페이지의 첫 화면을 바꿨다가, 다시 되돌렸다.
A/S 를 전문으로 하는 고객사의 홈페이지였다. 애드소프트 홈페이지를 만든 틀을 그대로 가져와 세웠고, 사진을 넣고, 첫 화면을 한 번 갈아엎었다가 원래대로 돌려놓았다. 쉽게 되돌릴 수 있었던 건 첫 화면이 어디에 들어 있고 무엇과 묶여 있는지가 처음부터 분명했기 때문이다. 두 번째 사이트는 결국 첫 번째 사이트의 구조가 맞았는지를 확인하는 시험이었다.
복사한 건 폴더가 아니라 자리였다
애드소프트 홈페이지는 네 가지 자리로 나뉘어 있다. 화면에 나가는 문구는 전부 src/i18n/ui.ts 에, 상호·대표·전화·주소 같은 회사 정보는 src/consts.ts 한 곳에, 페이지의 실제 생김새는 src/views/ 의 뷰에, 그리고 src/pages/ 의 페이지 파일은 언어를 정하고 뷰를 부르는 껍데기다. 메뉴는 NAV_ITEMS 배열 하나가 헤더와 하단을 같이 그린다.
두 번째 사이트는 이 자리를 그대로 썼다. 바뀐 건 자리 안에 들어가는 것들이다. 메뉴는 회사소개·서비스 다음에 포트폴리오와 공유도구 대신 파트너, 고객지원, A/S 진행 조회가 들어갔다. 색은 고객사 로고에서 다시 뽑았다. 문의 폼은 외부 폼 서비스로 보내던 것을 고객사가 쓰는 A/S 관리 시스템의 접수 API 로 보내게 바꿨다.
폼에서 바꾸지 않은 것도 있다. 원래 홈페이지의 문의 폼은 폼 서비스 키가 없으면 폼을 그리지 않고 이메일 안내를 보여 준다. 두 번째 사이트도 접수 API 주소가 비어 있으면 폼 대신 전화 안내를 띄운다. 보낼 곳이 정해지지 않은 상태에서 폼을 보여 주면 방문자는 신청했다고 믿고 떠난다. 그 상황만은 어떤 사이트에서도 만들지 않기로 했다.
언어 처리는 한 글자도 바꾸지 않았다. src/i18n/utils.ts 는 두 저장소에서 내용이 똑같다. 원래 홈페이지에서 영문 메인 주소가 /en// 로 나오던 문제를 먼저 고쳐 둔 덕에, 두 번째 사이트는 고친 판을 들고 시작했다.
처음에는 한국어만 열었다.
export const DEFAULT_LANG = "ko" as const;
/** 지금은 한국어만 운영한다. 영문을 열 때 여기에 "en" 을 추가하고 ui.ts 에 문구를 채운다 */
export const LANGS = ["ko"] as const;
export type Lang = (typeof LANGS)[number];
영문은 나중에 열었다. 한 일은 이 배열에 "en" 을 넣고, ui.ts 에 한국어와 같은 키 구조로 영문을 채우고, src/pages/en/ 아래에 껍데기 페이지를 만드는 것이었다. 사진 설명이나 서비스 카드처럼 문구가 데이터 안에 있던 곳은 { ko, en } 으로 바꿨다. 언어 전환 링크, hreflang, canonical 은 레이아웃이 이미 LANGS 를 돌며 만들고 있었으니 따로 손댈 게 없었다.
사진을 넣으려면 헤더부터 열어야 했다
원래 홈페이지에는 사진이 없다. 첫 화면은 흐린 원 하나를 깔고 그 위에 글자를 올린 게 전부다. A/S 회사의 홈페이지는 사정이 달랐다. 현장에서 일하는 회사라는 게 한눈에 보여야 했다.
사진은 src/data/photos.ts 에 목록으로 모았다. 무료 사진 서비스의 이미지 서버에서 바로 불러오고, 화면 폭에 맞는 크기를 고르도록 srcset 을 만드는 함수를 같이 뒀다. 사진 위의 글자는 흰색으로 바꾸고, 모바일에서는 사진 전체를, 넓은 화면에서는 글자가 있는 왼쪽만 어둡게 덮었다.
그런데 사진은 코드만으로 들어오지 않는다. 원래 홈페이지에서 들고 온 web.config 의 Content-Security-Policy 는 img-src 가 'self' data: 뿐이었다. 이대로면 다른 서버의 이미지는 브라우저가 아예 요청하지 않는다. 그래서 사진을 넣는 변경에 img-src 에 이미지 서버 주소 하나를 더하는 줄이 같이 들어갔고, photos.ts 맨 위에 "다른 주소의 이미지를 쓰면 CSP 에 그 주소를 추가해야 한다"는 주석을 남겼다. 다음에 이미지를 바꾸는 사람이 같은 데서 막히지 않게.
첫 화면을 바꾼 건 그다음이다. 사진 첫 화면 아래에는 A/S 처리 다섯 단계를 설명하는 구역이 있고, 그 옆에 휴대폰 모양의 'A/S 진행 현황' 예시 화면을 컴포넌트로 그려 넣었다. 이 휴대폰이 꽤 그럴듯해서, 아예 첫 화면으로 올려 봤다. 왼쪽에 문구와 버튼, 오른쪽에 휴대폰. 사진은 처리 과정 옆으로 내려갔다.
올려 놓고 보니 휴대폰이 할 일이 없어졌다. 처리 과정 옆에 있을 때 그 화면은 "맡긴 A/S 가 접수부터 완료 확인까지 고객에게 이렇게 보인다"는 설명의 일부였다. 첫 화면에서는 맥락 없이 떠 있는 예시일 뿐이었다. 사진도 마찬가지로, 첫 화면에서는 회사가 무엇을 하는지 보여 줬지만 처리 과정 옆에서는 장식이 됐다. 둘의 자리를 바꾸자 둘 다 덜 말하게 됐다. 그래서 되돌렸다. 되돌리는 건 간단했다. 첫 화면이 HomeView 한 파일 안에서 끝나고, 사진은 photos.ts 의 이름 하나로 불리기 때문에 변경을 통째로 뒤집으면 됐다.
그 뒤로 회사소개, A/S 서비스, 고객지원, 가맹 문의, 메인의 파트너 카드에 사진을 하나씩 넣었다. PHOTOS 에 항목을 더하고 뷰에서 이름으로 부르는 같은 방식이다.
이름을 빼고 쓰는 포트폴리오
다시 애드소프트 홈페이지로 돌아온다. 포트폴리오는 한 일을 보여 줘야 하는데, 한 일의 상당수는 이름을 밝힐 수 없다. 협력사 소속으로 참여한 프로젝트는 그 회사의 제품이고, 고객사의 업무 시스템은 그 회사의 사정이다.
원칙은 src/data/works.ts 맨 위에 적었다. 실제 진행한 프로젝트만 싣는다. 서비스명·고객사명·발주처는 적지 않고, 시스템의 성격과 기술 내용만 쓴다. 이름을 빼면 남는 게 있어야 하니 카드마다 대표 성과를 두세 줄로 적는다. "그룹웨어"라고만 쓰면 아무 말도 안 한 것이고, "그룹웨어가 OIDC 인증 서버 역할을 맡아 같은 도메인의 다른 서비스가 로그인 한 번으로 이어진다"고 써야 그 프로젝트에서 무엇을 풀었는지가 보인다.
프로젝트는 성격에 따라 네 갈래로 나눴다. 직접 수주한 고객사 프로젝트, 협력사 소속으로 참여한 프로젝트, 자체 기획한 서비스와 도구, 그리고 회사를 세우기 전 C/C++ 로 쌓은 경력 프로젝트다. 갈래마다 카드 배지 모양을 따로 뒀다. 참여한 프로젝트를 직접 수주한 것처럼 보이게 하지 않으려는 구분이다. 홈 화면의 대표 프로젝트는 목록 순서가 아니라 id 로 고정했다. 목록에 새 항목을 앞쪽에 끼워 넣어도 홈의 세 칸이 흔들리지 않는다.
이 방침은 포트폴리오 안내 문구에도 그대로 적었다.
회사 소개 옆에 둔 공유도구
포트폴리오 다음 메뉴는 공유도구다. 애드소프트가 만든 작은 도구를 누구나 내려받아 쓰게 하는 곳이고, 지금은 Windows 용 명령어 노트 앱 Cliply 하나가 있다.
페이지는 전부 src/data/tools.ts 의 TOOLS 배열에서 나온다. /tools/ 목록과 /tools/cliply/ 상세가 한·영으로 있고, 상세 페이지 맨 위에는 내려받기 상자가 있다. 버전, 파일 크기, SHA-256 이 보이고 SHA-256 옆 버튼을 누르면 클립보드로 복사된다. 안내 본문 끝의 "파일 확인" 구역에는 PowerShell 로 해시를 확인하는 명령을 같이 적었다. 실행 파일을 나눠 주는 곳이라면 받은 파일이 올린 파일과 같은지 확인할 길은 있어야 했다.
Cliply 가 0.2.0 에서 영어 화면을 갖추면서 스크린샷도 Record<Lang, ...> 으로 나눠, 영문 페이지에는 영어 화면과 영어 대표 이미지가 나간다.
블로그에 Cliply 사용 가이드와 개발기 연재가 올라간 뒤에는 상세 페이지 끝에 "블로그에서 더 보기" 구역을 붙였다. Tool 타입에 선택 항목 blogLinks 를 두고, 없으면 구역 자체를 그리지 않는다. 글은 한국어뿐이라 영문 페이지의 링크에는 "(in Korean)" 을 붙였다. 메뉴에도 기술블로그를 넣었다. 다른 사이트 주소라 영문 페이지에서도 /en/ 이 붙으면 안 되니, NAV_ITEMS 항목에 external: true 를 두고 헤더가 그 항목만 주소를 그대로 쓰게 했다.
루트에 놓이는 작은 파일들
홈페이지에는 사람이 보지 않는 파일도 있다. 루트에 놓여 검색엔진과 광고 크롤러가 찾아오는 파일이다. 하나는 ads.txt 다. 블로그에 광고를 붙이면서 애드센스에 사이트를 등록해야 했는데, 등록은 루트 도메인 addsoft.co.kr 단위로만 됐다. 블로그는 하위 도메인에 있다. 루트 도메인의 주인이 이 게시자라는 걸 루트에 놓인 ads.txt 로 밝히고, 같은 파일에서 하위 도메인을 가리켰다.
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, (인증 기관 ID)
subdomain=blog.addsoft.co.kr
subdomain=economy.addsoft.co.kr
첫 줄은 이 도메인의 광고 인벤토리를 이 게시자가 직접 판다는 선언이다. 아래 subdomain= 줄은 크롤러에게 이 하위 도메인의 ads.txt 도 읽어 보라고 알려 주는 줄이다. 루트의 파일 하나가 하위 도메인 블로그들을 잇는 목록이 되는 셈이라, 블로그를 하나 더 열었을 때 이 파일에 한 줄을 더했다. 홈페이지 자체에는 광고 스크립트를 넣지 않았다.
다른 하나는 구글 서치 콘솔의 소유 확인 파일이다. 구글이 정해 준 이름의 HTML 파일을 루트에 두면 된다.
두 파일 모두 public/ 에 넣었다. 배포 스크립트는 빌드 결과 dist/ 를 서버와 거울처럼 맞추고, dist/ 에 없는 서버 파일은 지운다. 서버에 직접 올려 둔 확인 파일은 다음 배포에서 조용히 사라지고, 소유 확인이 어느 날 풀린다. 그래서 루트에 있어야 하는 파일은 저장소의 public/ 에 있어야 한다.
같은 이유로 Cliply 의 zip 은 따로 지킨다. zip 은 용량 때문에 git 에 넣지 않는다. 그 파일이 없는 PC 에서 배포하면 거울 맞추기가 서버의 zip 을 지워 버린다. 그래서 배포 스크립트는 동기화 전에 tools.ts 에 적힌 내려받기 항목을 읽고, dist/ 에 그 파일이 같은 크기와 같은 SHA-256 으로 있는지 확인한다. 하나라도 어긋나면 배포를 멈춘다.
액자에 넣지 않는 페이지
마지막은 응답 헤더다. 홈페이지가 정적 사이트라고 서버 설정이 없는 건 아니다. public/web.config 가 빌드 때 dist/ 로 복사되어 IIS 설정이 되고, 보안 헤더도 여기서 나간다.
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<add name="X-Frame-Options" value="SAMEORIGIN" />
<add name="Content-Security-Policy" value="default-src 'self'; ...; frame-ancestors 'self'; base-uri 'self'" />
X-Frame-Options: SAMEORIGIN 과 CSP 의 frame-ancestors 'self' 는 같은 말을 두 번 한다. 이 페이지는 같은 출처의 페이지 안에서만 프레임으로 띄울 수 있다. 남의 사이트가 이 페이지를 투명한 액자에 넣고 그 위에 버튼을 겹쳐 클릭을 가로채는 걸 막는 헤더다. 두 번째 사이트에서 사진을 넣을 때 고친 img-src 도 이 CSP 한 줄 안에 있다. 블로그는 정적 사이트가 아니라서 같은 X-Frame-Options 를 ASP.NET Core 미들웨어가 응답마다 붙인다.
이 헤더가 한 번 내 발목을 잡았다. 애드센스의 광고 설정 화면에는 사이트를 미리 띄워 놓고 광고를 넣지 않을 영역을 고르는 기능이 있다. 그 미리보기가 끝내 뜨지 않았다. 미리보기는 구글의 화면 안에 내 사이트를 프레임으로 띄우는 방식이고, 홈페이지도 블로그도 같은 출처가 아닌 프레임은 거절하고 있었다. 헤더는 정확히 하라는 일을 하고 있었다.
헤더를 풀지는 않았다. 광고 설정 화면 하나를 쓰자고 모든 페이지의 프레임 방어를 내리는 건 맞바꿀 거래가 아니었다. 광고를 넣지 않을 페이지는 다른 방법으로 지정했고, 미리보기는 쓰지 않는 기능으로 남겼다.
회사 홈페이지는 처음에 회사를 소개하는 몇 장의 페이지로 시작했다. 지금은 만든 도구를 나눠 주고, 블로그로 사람을 보내고, 검색엔진과 광고 크롤러에게 이 도메인의 주인이 누구인지 밝히고, 남의 액자에 들어가기를 거절한다. 그리고 같은 틀이 다른 회사의 홈페이지가 되어 한 벌 더 돌아가고 있다. 정적 사이트라 화면 뒤가 비어 있을 줄 알았는데, 연재를 마치고 돌아보니 화면 뒤에서 하는 일이 화면만큼 많았다. 「Astro로 만든 회사 홈페이지」 연재는 여기까지다.