🗯️ episode
[주간 MILLO] EP.03 우리만의 기술 블로그를 만들다
안녕하세요! 밀로의 프론트엔드 개발자 고등어입니다. 팀 블로그를 만든 지 몇일정도 지난(ㅋㅋㅋㅋ) 시점에서 따끈따끈 ~
Notion을 CMS로 활용한 블로그를 만들면서 겪었던 이야기들을 들려드리려고 해요.!
💡 신기한 우연, 그리고 시작
사실 저와 악덕님(팀장)은 개인 블로그로 morethan-log를 커스텀해서 사용하고 있었어요.


이 템플릿을 선택했던 이유는 노션으로 손쉽게 콘텐츠를 관리할 수 있고, 다른 템플릿들보다 커스텀하기 훨씬 편리했기 때문이었죠.
그래서 저는 블로그에서 우리 팀의 이야기도 쓰면 너무 재밌겠다 싶어 주간 밀로라는 이름으로 1편을 쓰고있었어요. 근데 문득 ‘팀 블로그가 있으면 좋겠다’ 는 생각이 들었고, 정기 회의때 제안을 해볼까 고민하고있었는데, 우연히 악덕님과 다른 기술 이야기로 디스코드를 하다가 깜짝 놀랄일이 있었어요.
악덕님도 같은 생각을 하고 계셨던 거예요!
“그래서 팀 블로그를..”
“저 너무 소름돋아요 악덕님..” _ 그날의 기억.jpg
그렇게 저희는 토요일에 블로그 작업을 시작하기로 했고, 저는 “그럼 제가 일단 글은 다 써서 그때 보여드릴게요!” 라고 했죠.
이제 본격 작업시작 !
당연하게도 Morethan-log 선택😎
앞서 말했듯이 morethan-log를 선택한 이유는 명확했어요. Notion을 CMS로 활용할 수 있어서 글 작성과 관리가 정말 편리하거든요. 개발자들도 글쓰기에 집중할 수 있어야 하니까요! 또한 Next.js의 강력한 기능들도 활용할 수 있어서 커스터마이징하기 좋았답니다.
팀장님의 실시간 요구사항의 시작🙋
1. 전체조회에서 작성자 예쁘게 만들기



가연님 이거 포스트 상세내역에서 작성자 예쁘게 뜨는것처럼 전체조회에서도 똑같이 뜨게 해주세요
( • ̀ω•́ )✧오케이 ! 당연히 글쓴이가 전체조회에도 표시되어야죠 ! 바꿔봅시다 !
{data.author && data.author[0] && data.author[0].name && ( <> {/* 작성자 정보가 있을 때만 프로필 표시 */} <div className="author"> <Image css={{ borderRadius: "50%" }} // 이미지를 동그랗게 처리 src={data.author[0].profile_photo || CONFIG.profile.image} // 프로필 이미지가 없으면 기본 이미지 사용 alt="profile_photo" width={24} height={24} /> <div className="">{data.author[0].name}</div> // 작성자 이름 표시 </div> <div className="hr"></div> // 작성자 정보와 날짜 사이 구분선 </> )}
- 프로필 이미지와 이름을 나란히 배치해서 친근한 느낌을 주고 싶었어요
- 프로필 사진이 없는 경우를 대비해서 기본 이미지도 준비했고요
- 작성자 정보가 있을 때만 구분선(hr)이 보이도록 해서 깔끔한 레이아웃을 유지했죠
디자인 세부 조정
.author { display: flex; /* 프로필 이미지와 이름을 가로로 배치 */ gap: 0.5rem; /* 이미지와 이름 사이 간격 설정 */ align-items: center; /* 세로 중앙 정렬 */ } .hr { margin-top: 0.25rem; /* 위쪽 여백 */ margin-bottom: 0.25rem; /* 아래쪽 여백 */ align-self: stretch; /* 컨테이너 높이에 맞춤 */ width: 1px; /* 구분선 너비 */ background-color: ${({ theme }) => theme.colors.gray10}; /* 테마에 맞는 색상 */ }
- 프로필 이미지는 동그랗게 처리하고
- 이름과의 간격도 적절하게 조절했으며
- 글 제목과 작성일자와의 균형도 맞췄답니다
이렇게 작성자 정보를 스타일링해서 글의 신뢰도도 높이고, 시각적으로도 깔끔하게 보이도록 했어요 !
2. 카테고리 목록 띄우기 및 조회방식 선택하기 (그리드, 리스트)




가연님 카테고리도 Tags 위에 뜨게 해주세요 !
ㄴ 🐟 : ㄴ ㅖ , 어.. 근데 그리드 조회 방식이 있으면 리스트 조회 방식도 있어야하는거 아녜여?
ㄴ 🐤 : 오 그러네여 ? 바꿔주세여!
ㄴ 🐟 : …빡쎄겠지만 (•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ
사용자들이 원하는 방식으로 글을 찾아볼 수 있어야 편할것같았어요. 그리하여 해당 변경 사항은 이렇습니다!
- 왼쪽 사이드바에 카테고리 목록을 배치
- 각 카테고리 옆에는 해당 글 수를 표시
- 클릭하면 URL이 자동으로 업데이트되면서 필터링
- 모바일에서도 잘 보이도록 반응형으로 구현
- 그리드/리스트 뷰 구현
const CARDS_PER_PAGE = 5 // 그리드 뷰일 때는 5개 const LIST_PER_PAGE = 10 // 리스트 뷰일 때는 10개 const itemsPerPage = viewType === 'card' ? CARDS_PER_PAGE : LIST_PER_PAGE;
- 상단에 아이콘으로 된 전환 버튼 추가
- 그리드는 한 페이지에 5개씩 카드 형태로
- 리스트는 한 페이지에 10개씩 컴팩트하게
- 선택한 보기 방식은 sessionStorage에 저장
- 스크롤 위치 관리
근데 위의 방식을 구현하다 우리는 또 다른 UX 문제점을 찾았습니다. 상세페이지에서 뒤로가기를 하게 되면 내가 보고 있던 조회 방식을 기억하지 못하고 초기의 상태를 다시 로드 해준다는점이었어요. 그래서 이 부분을 스크롤 위치를 저장하는 방식으로 구현해보려고 했습니다.
const handlePageChange = useCallback((page: number) => { setCurrentPage(page); if (!sessionStorage.getItem('isBackNavigation')) { window.scrollTo({ top: 0, behavior: 'smooth' }); } sessionStorage.removeItem('isBackNavigation'); }, []);
하지만 스크롤 위치 관리 부분은 아직 완벽하게 구현하지 못했어요. 글 목록에서 페이지를 이동할 때는 맨 위로 스크롤되도록 했고 이전의 조회 방식을 저장하는것까진 했지만, 상세 페이지에서 뒤로 가기로 돌아왔을 때 이전 스크롤 위치를 기억하는 부분에서 어려움을 겪었답니다. sessionStorage를 활용해보기도 했지만 아직 완벽하게 동작하지 않네요. 추후에 더 개선해볼 예정이에요!
3. 썸네일 작업


가연님 근데 이거 링크 썸네일 어케 하는거에요 ?
ㄴ 🐟 : 메타태그 추가하면 돼요 ! 해둘게요 !
자 ~ 하는김에 SEO 최적화도 해볼까요 ? metatag를 쓰면 최적화가 쉬워집니다 !
<meta property="og:site_name" content={CONFIG.blog.title} /> <meta property="og:type" content="website" /> <meta property="og:title" content={CONFIG.blog.title} /> <meta property="og:description" content={CONFIG.blog.description} /> <meta property="og:image" content={ogImage} />
- Open Graph 태그로 SNS 공유 시 미리보기 최적화
- Twitter Card 메타 태그도 추가
- 구글/네이버 서치 콘솔 연동
처음에는 단순히 링크를 공유할 때 멋진 썸네일이 표시되게 하려는 목적이었어요. 하지만 이런 메타 태그들이 SEO에도 큰 영향을 미친다는 것을 알게 됐죠!
특히
og:image
설정에 신경을 많이 썼는데요, 우리 팀 로고가 공유 링크에 멋지게 표시될 수 있도록 절대 경로로 수정했어요:ogImageGenerateURL: "https://mil-lo.com/MILLO.png" // 전체 URL 경로로 변경
이렇게 하니 카카오톡이나 슬랙 같은 메신저에서 링크를 공유했을 때 훨씬 더 프로페셔널해 보이게 되었답니다. 사용자들의 클릭률도 높아질 것 같아요!
또한 소셜 미디어뿐만 아니라 검색 엔진에서도 우리 블로그가 잘 노출될 수 있도록 SEO 관련 설정도 추가했어요. 앞으로 우리 팀의 기술 콘텐츠가 더 많은 개발자들에게 도움이 되길 바라며 준비했답니다.
4. 캐시 문제 해결

이상하게 모든 디자인을 수정하고 보니 개발환경에서는 새로 작성한 포스트가 바로바로 적용이 됐지만 배포환경에서는 전혀 글이 올라오지않았어요. 고민을 해보았지만 아무래도 revalidateTime을 너무 낮게 설정해놔서 Next.js의 ISR(Incremental Static Regeneration)과 Notion API의 응답이 캐싱되는 것이 문제인것같았어요. 그래서 ? 저는 캐시를 지우는 로직도 넣기로 합니다.
// Notion API 호출 시 캐시 방지를 위한 코드 추가 const api = new NotionAPI({ activeUser: process.env.NOTION_ACTIVE_USER, userTimeZone: 'Asia/Seoul', authToken: process.env.NOTION_TOKEN }) // 타임스탬프 로깅 console.log(`Fetching posts at ${new Date().toISOString()}`)
그리고 revalidate 시간도 조정했어요:
return { props: { dehydratedState: dehydrate(queryClient), timestamp: Date.now(), // 타임스탬프 추가하여 매번 다른 값으로 만듦 }, revalidate: 1, // 1초로 설정 };
그리고 next.config.js에도 캐시 방지 헤더를 추가했죠:
async headers() { return [ { source: '/:path*', headers: [ { key: 'Cache-Control', value: 'no-store, must-revalidate, max-age=0', } ], }, ]; }
이렇게 여러 캐싱 레이어를 처리하는 과정이 정말 재미있으면서도 도전적이었어요. 실제 배포 환경에서 발생하는 문제를 해결하는 경험이 많은 공부가 되었답니다!
5. Hydration 에러 해결
SSR과 CSR 사이의 상태 불일치 문제를 해결하느라 꽤 애를 먹었어요. Next.js의 서버 사이드 렌더링에서는 sessionStorage에 접근할 수 없기 때문에, 조건부로 처리해야 했죠.
const [viewType, setViewType] = useState<'card' | 'list'>(() => { if (typeof window === 'undefined') return 'card'; const savedState = JSON.parse(sessionStorage.getItem('feedState') || '{}'); return savedState.viewType || 'card'; });
이렇게 우리만의 블로그를 만들어가는 과정이 정말 즐거웠어요. 기술적인 도전도 많았지만, 그만큼 배운 것도 많았죠. 앞으로 팀원들의 새로운 요구사항이 생기면 그때그때 개선해나갈 예정이에요. 우리 팀의 이야기가 이 블로그를 통해 잘 전달되었으면 좋겠네요!
다음에는 어떤 이야기로 찾아뵐지 기대해주세요! ✨
To be continued...
written by 고등어 (Frontend Developer)