신년 맞이 블로그 리뉴얼


우선 내 블로그는 Next로 개발했고, 전부 SSG
이다.
그래서 DB도 없다. 그저 로컬 파일의 md 파일을 변환해서 SSG로 만들고 Vercel
로 배포했다.
1. 다크모드 문제
큰 인식을 하고 있지는 않았지만, default theme mode가 light
모드여서
다크모드로 바꾸고 새로고침을 하면 클라이언트 사이드에서 localStorage
에서 가져오는 딜레이 때문에
잠깐 light 모드였다가 다크모드로 바뀌는 것을 발견했다.

화면이 깜빡이는 효과를 없애기 위해
서버사이드에서 사용자의 theme mode에 맞춰서 html을 내려주는 방법을 생각해보았다.
- request시 쿠키에 theme mode를 담아서 보낸 후 middleware에서 처리할까?
- dynamic rendering을 이용해서 html head 부분만 바꿔서 보낼까?
등등의 생각을 했지만, SSG로 만든 이상 구현이 힘들었다.
그래서
html 로드 전 theme mode 동기화 스크립트를 실행하도록 했다.
head 태그에 아래 스크립트를 심었다.
javascript(function () { const theme = localStorage.getItem('ryxxn-tech-blog-theme') || 'light'; document.documentElement.setAttribute('data-mode', theme); const classList = document.documentElement.classList; if (theme === 'dark') { classList.add('dark'); } classList.remove(theme === 'dark' ? 'light' : 'dark');})();
이제 새로고침을 했을 때 현재 theme이 바로 반영이 된다.
2. 카드 형식 UI 추가
기존에는 일반적인 블로그 목록 UI였지만, 카드 형태의 UI도 추가했다.

우선 다크모드의 경우, 카드 하단 부분 프레임을 썸네일의 대표 색상을 gradient로 주었다.
빌드시 sharp
라이브러리를 이용해서 이미지를 webp 확장자로 변환하는 과정을 거치고 있었다. 그래서 sharp
가 설치된 김에 이미지 메인 컬러 추출 함수를 만들었다.
tsximport sharp from 'sharp';import path from 'path'; export const getImageDominantColor = async ( relativePath: string): Promise<string> => { try { const absolutePath = path.join(process.cwd(), 'public', relativePath); // Sharp를 사용해 Dominant Color 추출 const { dominant } = await sharp(absolutePath).stats(); const { r, g, b } = dominant; return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; } catch (error) { console.error('Error extracting dominant color from WebP:', error); throw error; }};
이미지에서 dominant color 추출 후
tsx// ... <div style={{ '--gradient-color': thumbnailDominantColor } as React.CSSProperties} className={cx(styles.articleContent, 'p-4 h-full relative bg-white')} >// ...
style 변수로 넣어주고
css[data-mode='dark'] .articleContent { background: linear-gradient( to bottom, var(--gradient-color), rgb(53, 65, 91) );}
적용시켰다.
그렇지만 또 문제가 생겼다.
3. 목록 layout 동기화 문제
카드 UI와 리스트 UI를 선택했을 때, 새로고침시 어떻게 동기화시키지? 라는 문제가 생겼다.
localStorage에 현재 layout 상태를 담는다고 했을 때,
Client 사이드 컴포넌트로 만들면 새로고침시 분명 바로 적용이 되지 않을 것이다.
그렇지만. 방법을 생각해냈다.
기본에 충실하여 html attribute랑 css만을 이용하여 구현해보기로 했다. 그렇게 하면 다크모드 문제와 같이 해결이 가능할 것 같았다.
html<html lang="ko" data-theme="dark" class="dark" data-article-layout="grid"><!-- ... --></html>
localStorage에 grid
인지 list
인지 담고, html에 적용한다.
tsx{/* CSS 수준에서 하나는 display none, 다른 하나는 보여지게 함. */}<section className={cx(styles.gridLayout, 'grid 대충 어쩌고 class name들')}> {articles.map((article) => ( <CardArticle key={article.slug} article={article} /> ))}</section> <ul style={{ viewTransitionName: 'article-layout' }} className={cx(styles.listLayout, 'max-w-[700px] mx-auto')}> {articles.map((article) => ( <Article key={article.slug} article={article} /> ))}</ul>
그리고 css에서
css[data-article-layout='grid'] .listLayout { display: none;}[data-article-layout='list'] .gridLayout { display: none;}
하면 잘 작동한다.
다크모드 때처럼 head 태그에 스크립트를 추가하면 새로고침해도 즉시 반영된다.
그치만 이렇게 하면 문제가 하나 있었다.
SEO 측면에서 동일한 컨텐츠가 2개씩 뜬다는 것이었다.
그래서
일단 list
형식의 UI가 excerpt가 있기 때문에 정보가 더 많아서 default UI로 하고,
그래서 card
UI에
tsx// cart-article.tsx// ...<Link data-article-type="card" href={`/article/${slug}`} // card 타입만 구글 크롤러가 무시하게 하고 싶은데.. aria-hidden="true"// ...
display:none;
과 aria-hidden="true"
를 주면 크롤링 봇이 무시할까 했지만,
구글 크롤러가 aria attribute를 신뢰한다는 증거를 찾을 수 없었다..
그래서 그냥 aria-hidden
은 사용하지 않고 냅뒀다.
(사용하면 스크린 리더 측면에서도 고려해야 할 부분이 더 있었기에..)
4. hover시 layout이 깨지는 현상
card에 hover시 translateY(-8px)
이런식으로 줬는데,
transition시 미세하게 레이아웃이 깨진다.

처음에는 will-change: transform
을 사용하여 1차 해결.
그러나 원인을 모름.
며칠간 문제 원인 분석을 하고 나름의 결론을 냈는데,
아무리 검색을 해봐도 이 문제에 대한 명확한 근거를 찾을 수 없었다.
그래서 명확한 근거를 알고자
크로미움에 이슈 제기를 했다. 구글 크롬 렌더링 버그였다
이 문제에 대한 자세한 내용은 따로 글을 남길 예정이다.

5. 이미지 블러 효과 적용
좀 더 자연스러운 이미지 로드를 위해 Next Image
에서 지원하는 blur
기능을 적용했다.
그냥 blur
기능만 넣으면 되긴 했지만, 영 부드럽지가 않아 또 나름의 고민을 하여 애니메이션을 추가했다.
이것도 따로 글을 남겼다.

6. view transition api 적용
이것도 정말 힘들었다.
그저 화면 전환을 부드럽게 하고자 도입한 기능이지만, 또한 너무 힘들었다.
결과물만 보자면 일단 이렇다.

이것도 따로 글을 남겼다.

리뉴얼에 이렇게까지 힘을 쏟을 줄은 상상도 못 했다. 며칠 밤을 샌 건지 모를 정도로 참 힘들었다 . .