https://www.solid-connection.com/
우리 팀은 항상 커뮤니티 활성화에 대한 고민이 많았습니다.
저 또한 실제로 프로젝트에 합류한 이후 생각보다 질 좋은 내용이 커뮤니티에 존재해서 놀랬으며 교환학생을 준비하는 학생들에게는 매우 매력적인 질문들이 존재했습니다.
허나 이에 반해 커뮤니티의 조회수는 처참했습니다.
1. 모든 시작은 Google Analytics였습니다
커뮤니티 페이지는 기획초기 사용자수 확보를 위해 인증이 필요한 페이지로 개발되었습니다.
웹사이트 특성상 로그인을 시도하는 유저가 현저히 적기 때문에 매력적인 커뮤니티를 사용하기 위해서는 로그인이 필요하도록 설계했던 것입니다.
‘커뮤니티 활성화’라는 목표 아래 데이터를 들여다본 순간, 우리는 냉정한 현실과 마주했습니다.
→ 올해 1~ 11월

멘토기능은 10월에 베타 출시된 점을 고려하면 5개 네비게이션 바중 가장 중앙에 위치된 커뮤니티 탭의 조회수가 너무나도 낮다는 점이었습니다.
저희가 파악한 원인은 아래와 같습니다.
- 커뮤니티(
/community)로 향해야 할 사용자들이 로그인 페이지(/login)에 갇혀 있었습니다 - 커뮤니티 페이지의 실제 도달률은 기대에 한참 못 미쳤습니다
- 페이지가 인증이 필요하기에 SEO가 측정이 불가능합니다.
데이터가 말해주는 메시지는 분명했습니다. 사용자들은 커뮤니티의 가치를 전혀 경험하지 못한 채, ‘로그인’이라는 장벽 앞에서 이탈하고 있었습니다.
| /community/FREE | 70 (4.3%) | 18 (13.64%) | 3.89 | 15초 | 95 (3.31%) |
|---|
2. 사용자 관점에서의 질문: “무엇을 먼저 보여줄 것인가?”
커뮤니티 페이지는 커뮤니티 목록 페이지와 상세 페이지로 이루어져있습니다.

제가 교환학생을 준비하는 사람이라면 해당 질문과 같은 경우는 매우 궁금해서 클릭하고 싶게끔 질문이 작성되어있습니다.
따라서 저는 GA데이터를 토대로 우리 팀에게 근본적인 질문을 던지게 되었습니다.
“사용자가 커뮤니티의 가치를 경험하기도 전에, 왜 회원가입을 요구하고 있는가?”
저는 이문제를 해결하기 위해 팀에 1안을 제시했었습니다.


1안: 목록만 공개
- 보여줌: 제목/목록만
- 로그인 필요: 상세 보기 시
- 장점: 호기심 → 가입 전환 가능
- 단점: SEO·바이럴 불가, 가치 전달 전 가입 요구
2안: 전체 공개
- 보여줌: 제목 + 상세 본문 모두
- 로그인 필요: 글쓰기/댓글/좋아요 시
- 장점: SEO·바이럴·가치 전달 → 자발적 가입
- 단점: 정보만 얻고 이탈 → 단기 CVR↓ 가능
3. “가치를 먼저 맛보여주자”
매력적인 커뮤니티의 제목을 보고 클릭하면 로그인 하도록 유도한다면 사용자 입장에서도 로그인률도 높아지고 커뮤니티 활성화 측면에서도 뛰어날것이라 판단했습니다
따라서 우리는 1안, 제목/목록만 Public 공개를 선택했습니다.
이것은 단순한 기능 개발의 문제가 아니었습니다. 서비스의 철학과 장기 성장 전략을 결정하는 선택이 되었습니다.
선택의 이유:
- 장기적 유입 파이프라인(SEO + 바이럴) 구축
- “먼저 주고, 가치를 경험한 사용자가 자발적으로 참여하는” 선순환 구조
- 검색 엔진과 소셜 공유를 통한 지속 가능한 성장 전략
기대효용이론과 정보 비대칭 해소 관점
사용자는 어떤 행동을 할 때 “얻는 가치 > 드는 비용” 이라고 판단해야 움직입니다.
로그인은 분명한 비용이지만, 콘텐츠의 가치를 모르는 상태에서는 그 비용을 정당화하기 어렵습니다.
가치를 먼저 보여주어 기대 효용을 높이고,
정보 비대칭을 줄임으로써 로그인률을 높이는 전략
4. 성능 문제
Public 전환을 완료하고 WebPageTest로 성능을 측정한 순간, 우리는 새로운 문제와 마주했습니다.

[Before: 초기 비 로그인 적용 후]
- TTFB (서버 응답 시간): 1.413초
- LCP (최대 콘텐츠 렌더링): 2.967초
“문은 열어놨는데, 들어오는 데 3초나 걸린다니…”
SEO로 사용자를 데려와도 3초의 로딩 시간을 기다려줄 사람은 많지 않았습니다. 전략적 선택이 성능 문제로 무력화될 위기였습니다.
5. 기술적 해결: 전략을 현실로 만들기
5-1. RSC(React Server Components) 도입
우리 커뮤니티는 독특하게도 사용자가 많지 않아서 한두달에 한번 꼴로 커뮤니티 글이 올라오고 있습니다.
이러한 정적인 페이지에서는 가장 좋은 방법이 있습니다
바로 ISR 페이지입니다
ISR(Incremental Static Regeneration)
// Next.js 14+ App Router with RSC
export const revalidate = Infinity; // 1시간마다 재생성
async function CommunityPage() {
const posts = await fetchPosts(); // Server Component
return <PostList posts={posts} />;
}
ISR 페이지로 만들게 된다면 빌드시에 페이지가 정적으로 고정되기 때문에 사용자가 글을 올릴때마다 재 빌드를 해줘야합니다.
Next에서는 이러한 상황을 대비해서 revalidate를 지원합니다 .
따라서 next js의 revalidate api를 통해서 사용자가 커뮤니티 글을 작성하는 경우에만 다시 트리거 되도록 수정했습니다.
// boardCode가 있으면 해당 커뮤니티 페이지 revalidate
if (boardCode) {
revalidatePath(`/community/${boardCode}`);
revalidateTag(`posts-${boardCode}`);
return NextResponse.json({
revalidated: true,
message: `Community page for ${boardCode} revalidated`,
timestamp: Date.now(),
});
}
덕분에 사용자가 커뮤니티 페이지 진입시 서버와 네트워크 호출은 0에 수렴하게 되었습니다.
5-2. Hydration Boundary 최적화
우리 페이지는 무한스크롤 구조였기 때문에 초기 렌더링 시 필요한 데이터만 SSR로 받아오고
뷰포트 진입 시점에 따라 추가 fetch(react-query)를 진행해야 했습니다.
이를 위해 HydrationBoundary 를 사용했습니다.
HydrationBoundary는 서버에서 받아온 데이터를 react-query에 안전하게 연결해주는 브릿지 역할을 합니다.
Hydration
// ✅ Server Component
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import { getQueryClient } from "@/lib/queryClient";
import { fetchPost } from "@/api/posts";
import PostClient from "./PostClient";
export default async function PostContent({ postId }: { postId: string }) {
const queryClient = getQueryClient();
// 서버에서 prefetch
await queryClient.prefetchQuery({
queryKey: ["post", postId],
queryFn: () => fetchPost(postId),
});
const dehydratedState = dehydrate(queryClient);
return (
<HydrationBoundary state={dehydratedState}>
<PostClient postId={postId} />
</HydrationBoundary>
);
}
5-3. 가상화 무한 스크롤 구현
무한스크롤에서는 스크롤이 내려갈수록 렌더링할 DOM 노드가 기하급수적으로 증가한다는 문제가 있습니다.
이를 방치하면 브라우저 메모리 점유가 커지고, 스크롤·렌더링 성능 모두 저하됩니다.
이를 해결하기 위해 Virtualization(가상화) 를 적용했습니다.
화면에 보이는 아이템만 렌더링하고, 필요한 시점에 교체하기 때문에 불필요한 렌더링을 줄이고 전체 데이터 유지 비용 또한 최소화할 수 있습니다.
'use client';
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedPostList({ posts }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: posts.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // 각 아이템 예상 높이
overscan: 5, // 뷰포트 밖 5개 항목 미리 렌더링
});
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<PostItem
key={virtualItem.key}
post={posts[virtualItem.index]}
style={{ transform: `translateY(${virtualItem.start}px)` }}
/>
))}
</div>
</div>
);
}
6. 수치로 증명

[모든 성능 테스트는 https://www.webpagetest.org/에서 진행했습니다.]
[After: RSC + Hydration 최적화 + 가상화 적용]
- TTFB: 0.894초 (1.413초 → 37% 개선)
- LCP: 1.994초 (2.967초 → 33% 개선)
- 총 개선: LCP 약 1초 단축
하지만 더 중요한 성과는 숫자 너머에 있었습니다.
바로 비지니스 적인 사용자 관점의 가치입니다.
- 검색 엔진을 통한 자연 유입 발생 시작
- 사용자들의 자발적 콘텐츠 공유 증가
- 가치를 경험한 후의 회원가입으로 이탈률 감소
7. 회고
이 프로젝트에서 가장 어려웠던 순간은 “**1안을 선택하는 회의”**였습니다.
팀원 중 누군가는 이렇게 말했습니다: “정보만 보고 가는 사람들 많아지면 어떡하죠? 신규 회원가입수가 떨어지는 거 아닌가요?”
맞는 말이었습니다. 단기적으로는 1안이 더 안전한 선택일 수 있었습니다.
“우리가 만들고 싶은 커뮤니티는 어떤 모습인가?”
- 가입을 강요해서 억지로 키운 유령 커뮤니티?
- 아니면 가치를 먼저 경험한 사람들이 자발적으로 모이는 살아있는 커뮤니티?
답은 명확했습니다.
이 프로젝트가 제게 남긴 것:
- 데이터는 ‘문제’를 알려주지만, ‘해결 방향’은 결국 우리의 철학에서 나온다
- 좋은 기술은 좋은 전략을 실현하는 도구다
- 단기 지표에 흔들리지 않고 장기 비전을 선택하는 용기
그리고 무엇보다, LCP 1초를 단축한 것보다 **“이제 우리 커뮤니티가 검색되네요!”**라는 팀원의 말이 더 기뻤던 순간을 기억합니다.
좋은 개발은 기술을 위한 기술이 아니라고 생각합니다.
비즈니스 목표를 이해하고, 전략을 수립하고, 그것을 실현하기 위해 적절한 기술을 선택하는 것.
그것이 가장 큰 중요한 점이라는 생각이 들게 되는 과정이라고 생각합니다.
추후에 커뮤니티 이용자가 늘어났다면 !! 다시 글을 작성하러 오겠습니다
그 날이 오길 고대하며 …
