이 글에서 다루는 것
웹 페이지는 두 종류의 독자를 동시에 상대한다. 하나는 사람(눈으로 화면을 보고 스크롤하는 방문자), 다른 하나는 기계(검색엔진 크롤러, 링크를 미리보기로 펼치는 봇, 분석 도구). 오늘은 이 둘이 같은 사실을 보게 하면서도 코드는 지저분해지지 않게 정리하는 작업을 했다.
구체적으로 네 가지다. ① 로그인 안 한 방문자·검색봇에게도 홈이 읽을 거리를 갖게 열고, ② 로그인 정보를 아직 확인 중일 때 공개 페이지가 깨지지 않게 막고, ③ 분석 이벤트를 여러 도구에 한 번에 보내는 통로를 만들고, ④ 페이지가 넘어갈 때 넣는 애니메이션이 접근성·스크롤과 충돌하지 않게 껐다.
특정 제품·회사 이야기 없이, 그날 만난 범용 웹 개념만 정리한다.
하루 요약
| 작업 | 무엇을 하고 싶었나 | 한 일 | 결과 |
|---|---|---|---|
| 공개 홈·SEO | 검색봇·방문자가 홈에서 읽을 내용을 갖게 | 화면 글과 봇용 숨은 정보를 한 데이터에서 뽑음 | 빈약한 페이지·거짓 정보 방지 |
| 로그인 확인 대기 | 로그인 확인 중에 공개 페이지가 안 깨지게 | "확인 끝났고 + 실제 로그인" 일 때만 보호 API 호출 | 비로그인·검색 경로 안정 |
| 분석 통로 | 이벤트를 여러 분석 도구에 빠짐없이 보내려고 | 화면은 통로 하나만 부르고 뒤에서 여러 곳에 복사 | 도구마다 따로 부르는 실수 제거 |
| 페이지 전환 | 부드러운 화면 전환 + 접근성/스크롤 보존 | "애니메이션 싫어요" 설정이면 전환 자체를 끔 | 스크롤 충돌·테스트 깨짐 예방 |
1. 화면에 없는 정보를 봇용 마크업에만 몰래 넣지 않기
검색엔진은 사람이 보는 화면 글뿐 아니라, 페이지에 숨겨 둔 기계용 요약 정보도 읽는다. 이 숨은 정보와 화면 글이 서로 다르면 "속임수"로 취급돼 손해를 본다. 오늘 배운 건 둘을 항상 같게 유지하는 법이다.
배경지식
- 구조화 데이터(structured data) — ① 검색엔진이 페이지 내용을 빨리 이해하도록 넣는, 사람 눈엔 안 보이는 요약 정보. ② 없으면 검색 결과에 별점·FAQ 같은 풍부한 표시가 안 뜬다. ③ 흔히 JSON-LD(JSON 형식으로 쓴 이 요약)라는 스크립트로 넣는다.
- 가시 콘텐츠(visible content) — 즉, 사람이 실제 화면에서 눈으로 보는 글.
- thin content(빈약한 페이지) — 즉, 읽을 알맹이가 거의 없어 검색엔진이 저품질로 판단하는 페이지. 로그인 벽에 막혀 본문이 텅 비면 이렇게 된다.
- sitemap(사이트맵) — 즉, "우리 사이트에 이런 주소들이 있어요"라고 검색봇에게 건네는 주소 목록.
어떤 상황이었나. 로그인해야 볼 수 있는 페이지가 많다 보니, 비로그인·검색봇 눈엔 홈이 텅 비어 보이기 쉬웠다. 반대로 화면엔 안 보이는데 FAQ 요약 정보만 잔뜩 넣어두면 "속임수"가 된다.
무엇을 배웠나. 숨은 요약 정보는 화면 글과 반드시 일치시킨다. FAQ 목록을 데이터 하나로 두고 화면과 요약 정보가 그걸 같이 쓰게 하면, 화면에서 FAQ를 빼는 순간 요약에서도 자동으로 빠진다. 로그인 벽을 흐릿하게(블러) 처리하더라도 그 위에 진짜 제목·설명·FAQ 글을 실어 빈약한 페이지를 피한다. 사이트맵에는 실제로 열리는(정상 응답하는) 내부 주소만 넣고, 외부로 나가버리는 페이지는 억지로 넣지 않는다.
2. 로그인 확인이 끝나기 전엔 보호된 API를 부르지 않기
로그인이 필요한 데이터를 가져올 때, "로그인 정보를 아직 확인 중인 상태"와 "확인해 봤더니 비로그인인 상태"를 헷갈리면 공개 페이지가 엉뚱하게 로그인 화면으로 튕긴다. 이 둘을 구분하는 법을 배웠다.
배경지식
- 보호(된) API — 즉, 로그인한 사람만 부를 수 있는 서버 주소. 로그인 정보(쿠키) 없이 부르면 서버가 401(권한 없음) 에러를 돌려준다.
- 세션 로딩 — 즉, 브라우저가 "이 사람 로그인돼 있나?"를 서버에 물어보고 답을 기다리는 몇 순간. 이 동안엔 로그인 여부가 아직 모름이다.
enabled— 데이터 가져오기 라이브러리(예: React Query)에서 "이 조건이 참일 때만 요청 보내"라고 켜고 끄는 스위치. 즉,enabled가 거짓이면 API를 아예 안 부른다.- 목(mock) 서버 — 즉, 진짜 대신 미리 정해둔 가짜 응답을 주는 테스트용 서버.
어떤 상황이었나. 쿠키가 없는 방문에서 보호 API를 불러 401이 났고, "401이면 로그인으로 보내"라는 전역 규칙이 작동해 공개 홈까지 로그인 화면으로 튕겼다. 테스트는 가짜 서버가 늘 정상 응답을 줘서 이 문제를 못 잡았다.
무엇을 배웠나. "사용자 정보가 없음(user === null)"은 비로그인이 아니라 아직 확인 안 됨일 수 있다. 그래서 요청 스위치를 authResolved && isAuthenticated(로그인 확인이 끝났고 + 실제 로그인된 사용자일 때)처럼 두 조건을 함께 봐야 한다. 공개 페이지는 보호 API를 끄고 정적·가짜 데이터만 쓴다. 검증은 쿠키를 지운 진짜 브라우저로 한다.
3. 분석 이벤트는 통로 하나로 모아서 여러 곳에 보내기
"사용자가 버튼 눌렀다" 같은 기록을 여러 분석 도구에 보낼 때, 화면 코드가 각 도구를 직접 부르면 실수로 한 곳에만 보내거나 도구 오류가 결제 코드까지 터뜨린다. 중간에 통로 하나를 두는 패턴을 배웠다.
배경지식
- 분석 이벤트(analytics event) — 즉, "가입 완료", "결제 버튼 클릭" 같은 사용자 행동 기록. 이걸 모아 사용 통계를 낸다.
- 벤더 SDK — 즉, 분석 회사(구글 등)가 준, 이벤트를 그 회사 서버로 보내는 코드 묶음.
- 파사드(facade) 패턴 — ① 뒤에 여러 복잡한 것들이 있어도 앞에는 단순한 창구 하나만 두는 설계. ② 화면 코드가 도구 종류를 몰라도 되게 한다. ③ 은행 여러 부서 대신 창구 직원 한 명하고만 얘기하는 셈.
- 팬아웃(fan-out) — 즉, 한 번 들어온 걸 여러 목적지로 알아서 복사해 보내는 것.
어떤 상황이었나. 화면마다 분석 도구 코드를 직접 부르면, 어떤 화면은 한 도구에만 기록을 보내 통계가 어긋나고, 도구가 에러를 내면 그 예외가 결제·로그인 흐름까지 흔들 수 있었다.
무엇을 배웠나. 화면은 track(이벤트 기록) · identify(누구인지 알림) · reset(로그아웃 시 초기화)이라는 창구 함수 세 개만 안다. 그 뒤에서 실제 여러 분석 도구로 복사해 뿌린다. 각 도구는 환경설정으로 따로 켜고 끄며, 도구 호출이 실패해도 조용히 삼켜 본래 기능(결제 등)은 안 멈추게 한다. 어디서도 안 쓰는데 export만 된 헬퍼는 이사·정리할 때 되살리지 않는다.
4. "애니메이션 싫어요" 설정이면 페이지 전환을 앱에서 꺼 버리기
페이지가 넘어갈 때 부드럽게 겹쳐 사라지는 효과(cross-fade)를 넣고 싶었다. 그런데 어지럼증 등으로 애니메이션을 꺼 달라고 설정한 사용자에겐 이 효과가 오히려 해가 된다. CSS로 시간을 0으로 줄이는 것만으론 부족했고, 전환 기능 자체를 꺼야 했다.
배경지식
prefers-reduced-motion— ① 사용자가 OS/브라우저에서 "애니메이션 최소화"를 켰는지 알려주는 설정값. ② 어지럼·멀미에 민감한 사람을 위한 접근성 기능. ③ 켜져 있으면 화려한 움직임을 빼 줘야 한다.- JS 스프링 전환 vs CSS 전환 — 즉, 애니메이션을 자바스크립트가 계산해 움직이면(스프링), CSS에서 시간(
--duration)을 0으로 줄여도 JS 쪽은 그대로 살아 있어 안 꺼진다. matchMedia+useSyncExternalStore— 즉, 위 설정값이 바뀌는 걸 실시간으로 감지(matchMedia)해서 React 화면에 안전하게 반영(useSyncExternalStore)하는 방법.- Provider(프로바이더) — 즉, 특정 기능을 하위 화면 전체에 공급하는 감싸개 컴포넌트. 여기선 "페이지 전환 기능"을 켜는 스위치 역할.
- 가상 리스트(virtual list) — 즉, 긴 목록에서 화면에 보이는 몇 개만 실제로 그려 성능을 아끼는 기법. 스크롤 위치 관리가 예민하다.
어떤 상황이었나. 전역 전환 효과를 넣으니, 애니메이션은 자바스크립트가 돌려 CSS 시간만 0으로 줄여선 안 꺼졌고, 긴 목록의 스크롤 위치나 화면 비교 테스트(VR)와 충돌할 위험이 있었다.
무엇을 배웠나. "애니메이션 최소화" 설정을 실시간 구독해서, 켜져 있으면 전환 기능 공급자(Provider)를 아예 안 켠다(마운트하지 않는다). 그리고 전환 효과는 보이는 것만 담당하고 스크롤 위치는 손대지 않게 해서, 목록이 스스로 스크롤을 복원하는 권한과 충돌하지 않게 분리한다.
→ prefers-reduced-motion과 JS 페이지 전환 게이트