초기에 개발한 개인 블로그 : https://sunub.github.io/
초기에 개발한 블로그는 Github Page 를 이용하여 배포하였습니다. 초기 블로그는 SSG를 이용하여 MarkDown 파일로 작성한 파일을 외부로 노출 시키고 컬러 시스템을 제작하여 LightMode와 DarkMode를 구현하였습니다. 또한 모바일 환경에서 블로그를 이용할 수 있게끔 반응형 웹 디자인을 적용하였습니다.
하지만 초기의 블로그에는 다음과 같은 단점이 존재하였습니다.
- Github Pages는 정적 사이트 배포만 가능하므로, 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR)의 차이점을 학습하고 경험해 볼 수 없습니다
- 순수 정적 사이트만 생성이 가능하므로 Database를 활용하여 각 페이지마다 동적으로 메타 태그등을 관리할 수 없어 SEO를 향상 시키는 경험을 할 수 없습니다.
블로그를 제작하는 이유는 학습한 내용을 포스팅하기 위한 목적이 있었지만 학습과 다양한 경험을 해보기 위해서 제작하는 것이었기 때문에 vercel로 배포 환경을 변경하기로 결정하였습니다.
- GSAP 를 활용한 정밀한 제어, 최적화된 성능, 다양한 이징 효과를 통해 복잡하고 동적인 애니메이션을 제작 하였습니다.
- 디자인 목업에서 구현된 애니메이션을 성능 저하 없이 구현하기 위해 CSS 기반의 Sprite Animation 기법을 적용 하였습니다.
- 다양한 디바이스 환경에 최적화하기 위해, container query를 활용하여 컴포넌트 크기를 동적으로 조정하게끔 하였습니다.
- 쿠키 기반 사용자 선호도를 관리하여, 방문 시 자동으로 개인화된 환경을 제공하고, 서버 부하를 줄이며 일관되고 빠른 사용자 경험을 보장 하였습니다.
- MDX를 사용하여 특정 블로그 포스트에 관련한 컴포넌트를 직접 경험할 수 있게끔 하였습니다.
sunub.github.io
.
└── frontend
├── tests
│ ├── bench
│ ├── unit
│ └── e2e
└── fx_utils
이 프로젝트에서 수행한 테스트는 bnech, unit, e2e 테스트로 나누어져 있습니다. 각각의 테스트는 다음과 같은 목적을 가지고 있습니다.
- bench : 블로그의 메인 기능 중 하나인
.mdx파일을 읽고 처리하는 작업에 대해서 Streaming 을 이용하는 방식과 일반적인 파일 읽기 방식을 비교하는 벤치마크 테스트를 임의로tests폴더에 250000개의.md파일을 생성하고 이를 읽고 처리하는 시간을 측정하여 성능을 비교하였습니다. - unit : 블로그의 주요 기능인 최신에 등록한 포스트, 각 카테고리, 각 카테고리와 slug에 해당하는 포스트를 제대로 읽어오는 지 기대되는 컴포넌트, RSC를 제대로 렌더하는지에 대한 테스트를 수행합니다.
- e2e : 블로그의 주요 기능인 검색, 무한스크롤, 키보드 네비게이션, 접근성, 라우팅, 테마 전환등에 대한 테스트를 playwright를 이용하여 실제 브라우저 환경에서 테스트를 수행합니다.
테스트 자동화를 수행하면서 가장 힘들었던 점은 Flaky tests를 해결하는 것이었습니다. Flaky tests는 테스트가 간헐적으로 실패하는 현상을 말합니다. 이 현상이 정말 괴롭게 했던 것은 어느 경우에는 테스트를 통과하고 어느 경우에는 테스트가 실패하는 현상이 반복되었기 때문입니다. 그래서 애플리케이션 코드가 잘못된 것인지 테스트 코드가 잘못된 것인지를 확실하게 파악하기에 어려웠습니다.
Flaky tests 현상을 테스트 하기 위해서 --repeat-each 옵션과 --workers=5 와 같이 테스트를 여러 워커를 활용하여 동시다발적으로 테스트를 수행하기 때문에 각 페이지간의 이동이나 작업을 수행하는 과정이 명확하게 구분이 되지 않을 경우 Race Condition이 발생하여 테스트가 실패하는 현상이 발생하였습니다. 이 문제를 해결하기 위해서는 다음과 같은 방법을 사용하였습니다.
- 웹의 접근성을 보장할 것
이전까지는 테스트를 제대로 수행하기 위해서 웹의 접근성을 지켜야 한다는 말에 크게 공감되지 않았습니다. 하지만 테스트 자동화를 수행하면서 웹의 접근성을 제대로 지키지 않으면 테스트가 실패하는 현상을 겪으면서 웹의 접근성을 지키는 것이 얼마나 중요한지를 깨닫게 되었습니다. 예를 들어, 모달 창을 열고 닫는 기능을 테스트할 때 playwright가 권장하는 페이지 내부에서 특정 요소를 찾는 방법은 특정 role, test-id, aria-label등을 활용하여 요소를 찾는 방법입니다. 만약 이러한 속성들이 제대로 설정되어 있지 않으면 playwright가 요소를 찾지 못하여 테스트가 실패하는 현상이 발생합니다. 따라서 웹의 접근성을 보장하는 것은 테스트 자동화를 수행하는 데 있어서 매우 중요한 요소라는 것을 깨닫게 되었습니다.
웹의 접근성을 지킬 경우 구현 방식에 종속되지 않고 컴포넌트의 역할이 수행해야 할 기능에 대한 테스트를 수행할 수 있기 때문에 테스트의 안정성을 위해서라도 웹 접근성을 지키는 것이 중요하다는 것을 깨닫게 되었습니다.
- 명시적 대기를 적극적으로 활용할 것
Flaky tests 현상을 해결하기 위해서 가장 효과적이었던 방법은 명시적 대기를 적극적으로 활용하는 것이었습니다. 예를 들어, 특정 요소가 나타날 때까지 기다리는 코드를 추가하거나, 특정 애니메이션이 완료될 때까지 기다리는 코드를 추가하는 등의 방법을 사용하였습니다. 이러한 방법을 사용하면 테스트가 실패하는 현상을 줄일 수 있습니다.
이러한 기능을 수행할때는 playwright에서 제공하는 locator을 최대한으로 활용하고 toBeVisible등의 메서드를 활용하여 playwright가 제공하는 기본적인 기능을 최대한 활용하는 것이 좋습니다. 만약 이러한 메서드로도 해결되지 않는다면 toPass 메서드를 활용하여 특정 조건이 충족될 때까지 재시도하는 방법을 사용하였습니다. 이 방법은 특정 요소가 나타날 때까지 기다리는 것보다 더 안정적인 방법입니다.
- 테스트 결과 분석 도구를 적극적으로 활용할 것
단순히 테스트 실패 여부만 확인하는 것을 넘어, Playwright의 Trace Viewer와 같은 도구를 활용하여 테스트 실패 당시의 DOM 스냅샷, 네트워크 요청, 콘솔 로그를 상세히 분석했습니다. 이를 통해 "내 컴퓨터에서는 잘 되는데?"와 같은 막연한 상황을 벗어나, 실패 원인을 데이터에 기반하여 정확하고 빠르게 파악할 수 있었습니다.
- GSAP 애니메이션 타이밍: 포스트 목록의 등장 애니메이션이 완료되기 전에 테스트가 실행되어 요소를 찾지 못하는 문제
- Next.js SSR 렌더링 지연: 서버사이드 렌더링과 하이드레이션 과정에서 발생하는 예측 불가능한 지연
- 비동기 검색 API 호출: debounce 처리된 검색 요청과 응답 타이밍의 불일치
- 무한스크롤 IntersectionObserver: 스크롤 이벤트와 새로운 콘텐츠 로딩 간의 타이밍 불일치
이 문제를 해결하기 위해서는 의도적으로 약간의 지연을 통해 대기 시간을 확보하거나, 특정 요소가 나타날 때까지 기다리는 코드를 추가하는 등의 방법을 사용하였습니다. 이러한 방법을 사용하면 테스트가 실패하는 현상을 줄일 수 있습니다.
const [fileContent, _] = await Promise.all([
readMDXContent(filePath),
new Promise(resolve => setTimeout(resolve, minimumTimeout)),
]);위의 로직이 권장되지 않은 방법이라는 것은 알지만 사용자에게 대기 시간동안 로딩 애니메이션을 보여주고 페이지로 전환되는 방식이 사용자 경험을 망치지 않을 것이라고 판단했습니다. 이 판단은 사용자의 인지에 따른 시간에 따르면 1초라는 짧은 시간의 지연은 사용자 경험을 해치지 않는다는 연구 결과를 바탕으로 내린 판단이었습니다.
이러한 노력의 결과, 프로젝트의 테스트 파이프라인은 눈에 띄게 안정화되었습니다. 초기에는 10번 중 2-3번 실패하던 E2E 테스트 성공률이 99% 이상으로 향상되었습니다. 이제 테스트 실패는 Flaky Test가 아닌 실제 코드의 회귀(Regression)나 버그를 의미하게 되었습니다.
테스트 안정성을 위해 웹 접근성을 준수하고, 비동기 로직을 명확하게 처리하는 등의 노력이 자연스럽게 더 견고하고 예측 가능한 애플리케이션 코드로 이어졌습니다. Flaky Tests와의 싸움은 단순히 테스트 코드를 수정하는 것을 넘어, 테스트 자동화를 바라보는 관점을 바꾸는 계기가 되었습니다. 이전에는 테스트를 '기능 검증을 위한 도구'로만 생각했다면, 이제는 **'애플리케이션과 함께 성장하고 관리되어야 할 또 하나의 제품'**으로 인식하게 되었습니다.
특히 setTimeout을 이용한 최소 대기 시간 확보 결정은 중요한 교훈을 주었습니다. 모든 것을 순수하게 기술적인 관점에서만 해결하려는 집착을 버리고, 사용자 경험(UX)과 개발 경험(DX), 그리고 테스트 안정성 사이의 균형점을 찾는 것이 실용적인 엔지니어링이라는 것을 깨달았습니다. 테스트는 실제 사용자의 경험 흐름을 최대한 존중하며 설계되어야 한다는 것을 배울 수 있었습니다.
- 쿠키 기반 사용자 경험 개인화: SSG의 명백한 한계 사용자가 페이지를 요청하면, SSR(Server-Side Rendering) 서버는 즉시 요청 헤더의 쿠키를 읽어들입니다. 이 쿠키 정보를 바탕으로 해당 사용자의 테마가 완벽히 적용된 HTML을 처음부터 생성하여 전달합니다. 덕분에 클라이언트 단에서 테마가 바뀌면서 화면이 깜빡이는 플리커링(flickering) 현상 없이 매끄러운 사용자 경험을 제공할 수 있습니다.
반면 **SSG(Static Site Generation)**는 빌드 시점에 모든 사용자를 위한 단일 버전의 정적 HTML을 생성합니다. 이 시점에는 미래에 접속할 특정 사용자의 쿠키 값을 전혀 알 수 없습니다. 따라서 서버에서 미리 테마를 적용하는 것이 원천적으로 불가능하며, 클라이언트 측에서만 테마 적용이 가능해 필연적으로 테마 깜빡임이 발생하게 됩니다.
- 서버 중심의 효율적인 데이터 처리 및 상태 관리 SSR 환경에서는 서버 중심의 효율적인 데이터 처리와 상태 관리가 가능합니다. 예를 들어, Generator 함수를 통해 대용량의 포스트 목록도 요청 시 필요한 만큼만 순차적으로 평가하여 서버의 메모리 사용을 최소화할 수 있습니다. 이는 요청마다 자원을 효율적으로 사용해야 하는 SSR 환경에 최적화된 방식입니다.
또한, 서버 컴포넌트에서 Promise 기반으로 데이터를 가져오고, 클라이언트에서는 Suspense로 로딩 상태를 처리하는 등, 데이터 페칭부터 UI 렌더링까지의 비동기 흐름을 자연스럽게 통합하여 관리할 수 있습니다. 하지만 SSG는 빌드 과정에서 데이터를 모두 처리하므로, 요청 시점의 동적인 서버 자원 최적화라는 개념이 적용되기 어렵습니다.
- SEO 최적화 및 사용자별 콘텐츠 제공 SSR은 검색 엔진 크롤러에 최적화된, 내용이 모두 채워진 HTML을 제공하므로 SEO(검색 엔진 최적화)에 매우 유리합니다. 더 나아가, 서버는 각 사용자의 요청(쿠키, 세션 등)에 따라 개인화된 콘텐츠나 인증 상태에 따른 화면을 동적으로 구성할 수 있습니다. 이는 모든 사용자에게 동일한 페이지만 제공하는 정적 방식(SSG)의 한계를 극복하는 핵심적인 장점입니다.
이러한 이유들로 인해 Static Site Generator가 아닌 SSR을 주로 활용하여 개인 블로그를 개발했습니다. 물론 SSG를 사용하는 방법이 블로그를 개발하는 데 있어서 더 간단하고 빠른 속도로 개발이 가능하다는 것을 알고 있었지만, 블로그를 직접 개발하며 다양한 경험을 하고 SSR을 어떻게 활용하고 개발해야 하는지를 경험하기 위해 이러한 선택을 내렸습니다.
-
한글 검색 정확도 향상: 한글은 초성, 중성, 종성 조합으로 이루어집니다. "ㄱ" 검색 시 "가", "강" 등 관련 결과가 모두 검색되도록 한글 조합형 특성을 반영한 전처리 및 매칭 알고리즘을 적용했습니다. 이를 통해 사용자가 검색어를 정확히 모르거나 오탈자가 있어도 원하는 정보를 찾을 수 있습니다.
-
직관적인 영어 검색: 영어 검색 시에는 대소문자 구분 없이 소문자로 변환하여 일치 여부를 판단합니다. 사용자가 대소문자를 신경 쓰지 않고 자연스럽게 검색할 수 있습니다.
-
신속하고 효율적인 검색 환경: 사용자가 빠르게 검색어를 입력할 때, 불필요한 네트워크 요청을 줄이기 위해 debounce(지연 호출)와 AbortController를 활용했습니다. debounce는 일정 시간 입력이 없을 때만 검색 요청을 보내고, AbortController는 이전 요청을 취소하여 항상 최신 결과만 표시되도록 합니다.
-
한글 전용 비교 함수 구현: 한글의 조합 특성을 처리하기 위해 isKr, krList, eqKr, eqKrPos 등 한글 전용 비교 함수들을 직접 구현했습니다. 이는 초성, 중성, 종성 조합까지 세밀하게 비교하여 초성 검색 및 유사 매칭을 가능하게 합니다.
-
자동 언어 판별 및 맞춤형 로직: 검색 API는 사용자의 검색어가 한글인지 영어인지 자동으로 판별합니다. 이를 통해 각 언어의 특성에 최적화된 검색 로직을 적용합니다.
-
효율적인 네트워크 요청 관리: debounce 기법을 적용하여 불필요한 API 호출을 줄이고, AbortController를 사용하여 이전 요청을 취소함으로써 네트워크 자원의 낭비를 줄이고 최신 정보를 신속하게 제공합니다.
끊김 없는 콘텐츠 탐색
- 사용자가 스크롤을 내릴 때마다 자동으로 추가 포스트가 로드되어, 페이지 이동이나 버튼 클릭 없이 자연스럽게 더 많은 콘텐츠를 탐색할 수 있습니다.
- 이를 통해 사용자는 원하는 정보를 빠르게 찾을 수 있고, 읽던 흐름이 끊기지 않아 몰입감 있는 경험을 제공합니다.
로딩 상태 시각화 및 피드백
- 추가 콘텐츠를 불러오는 동안 로딩 컴포넌트를 표시하여, 사용자가 기다리는 상황을 명확하게 인지할 수 있도록 했습니다.
- 데이터가 모두 로드된 경우에는 더 이상 불필요한 요청이 발생하지 않도록 UX를 설계했습니다.
중복 데이터 방지 및 일관된 리스트 제공
- 이미 로드된 포스트와 중복되는 데이터는 필터링하여, 항상 새로운 콘텐츠만 추가로 보여줍니다.
- 이를 통해 사용자는 중복 없이 깔끔한 리스트를 경험할 수 있습니다.
IntersectionObserver와 throttle 활용
- 스크롤 하단에 위치한 요소가 화면에 나타날 때마다 IntersectionObserver를 통해 감지하고, throttle(0.5초 제한)로 이벤트 과다 발생을 방지하여 성능 저하 없이 자연스러운 무한 스크롤을 구현했습니다.
비동기 데이터 로딩 및 상태 관리
- 추가 데이터 로딩 시 React의 useTransition을 활용해 UI의 반응성을 높이고, 네트워크 요청 중에는 로딩 상태를 관리하여 사용자에게 명확한 피드백을 제공합니다.
|
|
모든 디바이스에서 최적의 가독성 제공
- 다양한 화면 크기(데스크탑, 태블릿, 모바일)에 맞춰 레이아웃과 폰트, 요소 크기를 자동으로 조정하여, 어떤 환경에서도 콘텐츠를 편안하게 읽을 수 있습니다.
모바일 환경에 특화된 UI/UX
- 모바일에서는 리스트, 버튼, 입력창 등 주요 UI 요소의 크기와 배치를 최적화하여 터치 조작이 쉽고, 스크롤이나 네비게이션이 자연스럽게 동작하도록 설계했습니다.
불필요한 요소 숨김 및 단순화
- 작은 화면에서는 복잡한 네비게이션이나 부가 정보를 숨기고, 핵심 콘텐츠에 집중할 수 있도록 UI를 단순화하여 사용자의 몰입도를 높였습니다.
CSS 컨테이너 쿼리 및 미디어 쿼리 활용
- @container와 @media 쿼리를 적극적으로 활용하여, 화면 크기와 방향에 따라 레이아웃, 패딩, 폰트, 이미지 크기 등을 동적으로 변경합니다.
반응형 레이아웃 구조 설계
- 주요 컴포넌트(리스트, 카드, 네비게이션 등)는 grid/flexbox 기반으로 설계되어, 화면 크기에 따라 자동으로 재배치됩니다.
모바일 전용 스타일 적용
- 모바일 환경에서는 최대 너비 제한, 패딩 조정, 버튼 크기 확대 등 모바일 친화적인 스타일을 별도로 적용하여 터치 조작과 가독성을 높였습니다.






