
기술연구소 팀블로그는 어떻게 만들어졌을까
들어가며
회사 기술 블로그를 만드는 방식은 보통 세 갈래로 나눌 수 있다. 회사가 직접 운영하는 기술 블로그, Medium이나 Velog 같은 외부 플랫폼, 그리고 엔지니어 개인 블로그다.
회사가 직접 기술 블로그 사이트를 만들고 에디터와 발행 도구까지 제공하면, 엔지니어들은 같은 포맷 안에서 글을 쓰고 운영자는 전체 글의 톤과 형식을 맞추기 쉽다. 대신 블로그 시스템 자체를 계속 관리해야 한다.
Medium, Velog 같은 외부 블로그 플랫폼을 쓰면 가장 빨리 시작할 수 있고, 글쓰기 도구나 검색, 공유 기능도 이미 갖춰져 있다. 대신 회사가 원하는 구조나 디자인, 배포 방식에 맞추기는 어렵다.
엔지니어들이 각자 블로그를 운영하면 자유도는 가장 높다. 누군가는 블로그 엔진을 직접 만들 수 있고, 누군가는 익숙한 도구를 쓸 수 있지만 글이 흩어지기 때문에 팀이 어떤 문제를 겪었고 어떤 판단을 해왔는지 한눈에 보기 어렵다.
Aijinet 기술연구소 블로그는 세 번째 방식에서 출발했다. 개인 블로그의 자유도는 살리고 싶었지만, 글이 완전히 흩어지는 것도 원하지 않았다. 기술연구소의 대문에서는 팀원들의 글을 한 번에 볼 수 있어야 했다.
이때 떠올린 것이 예전 팀블로그였다. 필자가 기억하는 2005년 무렵의 팀블로그는 여러 사람이 하나의 블로그 공간에 글을 쓰는 방식이었다. 몇 가지 테마나 템플릿을 제공하면 사용자는 그 안에서 선택했고, 데이터는 하나의 블로그 시스템 안에 저장됐으며 운영 방식도 그 시스템의 규칙을 따랐다.
이번에는 그 방식을 그대로 복원하고 싶지는 않았고, 가져오고 싶었던 건 하나였다.
여러 사람이 각자의 글을 쓰되, 독자는 하나의 팀 블로그처럼 읽을 수 있게 한다.
이 문장이 Aijinet Tech Blog의 시작점이었다.
예전 팀블로그와 다르게 하고 싶었던 것
예전 방식의 팀블로그는 한 공간 안에 여러 필자가 들어와 같은 엔진과 관리자 화면을 쓰고, 정해진 템플릿 안에서 글을 발행하는 구조에 가까웠다.
그 방식에도 장점은 있다. 운영자는 전체 글을 통제하기 쉽고, 독자는 일관된 화면에서 콘텐츠를 읽기 때문에 회사 기술 블로그처럼 외부에 보여주는 채널이라면 이 일관성이 도움이 된다.
기술연구소에서 바란 건 조금 달랐다. 엔지니어가 글만 쓰는 것이 아니라 자신의 블로그 자체를 직접 만들어보는 경험도 가져가길 바랐고, 어떤 프레임워크를 쓸지, 어떤 디자인을 할지, 글 상세 화면과 코드 블록을 어떻게 보여줄지까지 각자 판단할 수 있기를 원했다.
그래서 대문 사이트가 블로그 엔진 역할을 하지 않게 했다. Aijinet Tech Blog는 글을 작성하는 도구도, 글을 저장하는 CMS도 아니다. 기술연구소의 대문이고, 팀원 블로그의 최신 글을 모아 보여주는 허브다.
각 엔지니어는 자신의 블로그를 직접 구현한다. Next.js를 써도 되고, 다른 정적 사이트 생성기를 써도 되며 디자인도 자유롭게 가져갈 수 있다. 팀블로그 대문에 노출되려면 최소한의 계약만 지키면 된다.
그 계약은 두 가지다.
- 글은
md또는mdx기반으로 관리한다. - 개별 블로그는 RSS 목록을 발행한다.
이 정도만 맞추면 대문 사이트는 각 블로그가 어떤 엔진으로 만들어졌는지 몰라도 된다.
왜 이런 구조가 필요했나
기술연구소에서는 서비스 운영과 프로젝트 진행 과정에서 여러 판단이 생긴다. 챕터별로 겪는 문제도 다르다. AI Chapter에서는 모델 활용과 실험 과정의 고민이 쌓이고, DevOps Chapter에서는 배포와 운영 경험이 쌓이며, Backend와 Frontend에서도 각자의 시행착오가 생긴다.
이런 경험은 쉽게 지나간다.
PR에는 코드 변경이 남고, 이슈에는 작업 단위가 남으며, 회의록에는 결정 사항도 남는다. 하지만 실제로 어떤 점을 배웠는지, 다음에 같은 문제를 만나면 무엇을 다르게 할지, 그 판단의 배경이 무엇이었는지는 따로 정리하지 않으면 사라진다.
팀블로그는 그 빈자리를 채우기 위한 시도였다. 글을 쓰면 회고가 되고, 구현하면서 애매했던 부분이나 선택의 이유를 문장으로 다시 설명해야 하기 때문에 개인의 경험이 팀의 산출물로 바뀐다.
그렇다고 모든 엔지니어를 하나의 글쓰기 도구 안에 넣고 싶지는 않았다. 각자 블로그를 만들고 운영하는 쪽이 더 맞다고 봤고, 블로그 엔진을 직접 만드는 일도 하나의 기술적 산출물이기 때문이다.
그래서 구조는 이렇게 잡았다.
flowchart LR
landing["Aijinet Tech Blog<br/>기술연구소 대문"]
chad["/chad<br/>개별 블로그"]
sean["/sean<br/>개별 블로그"]
max["/max<br/>개별 블로그"]
rss["rss.xml"]
posts["최근 포스트"]
members["Chapter 소개"]
chad --> rss
sean --> rss
max --> rss
rss --> landing
landing --> posts
landing --> members
루트에는 기술연구소 대문이 있고, 그 아래에는 팀원별 블로그가 독립적으로 붙는다. 대문은 각 블로그를 소유하지 않고, RSS를 읽어 최신 글을 연결한다.
CMS가 아니라 RSS를 선택한 이유
팀블로그를 만든다고 해서 반드시 CMS를 만들어야 하는 것은 아니며, 이 프로젝트에서 CMS는 과한 선택이었다.
CMS를 만들면 글 작성, 수정, 발행, 이미지 업로드, 권한 관리, 미리보기, 임시 저장 같은 기능을 고민해야 한다. 회사 기술 블로그를 하나의 공식 매체로 운영한다면 필요할 수 있지만, 우리가 먼저 하려던 일은 글쓰기 도구를 만드는 것이 아니라 글을 연결하는 일이었다.
이미 각 엔지니어가 자신만의 방식으로 블로그를 만들 수 있다면, 대문 사이트가 할 일은 단순해진다.
- 어떤 블로그가 연결되어 있는지 알고
- 각 블로그의 RSS를 읽고
- 글에 작성자와 챕터 정보를 붙이고
- 최신순으로 보여주면 된다.
그래서 data/feeds.json에는 RSS 주소와 멤버 ID만 둔다.
{
"feeds": [
{
"url": "https://tech.bodoc.co.kr/chad/rss.xml",
"memberId": "chad"
}
]
}
이 파일은 대문 사이트가 외부 블로그를 어떻게 읽을지 정의한다. data/members.json은 팀과 멤버 정보를 담당하는데, 멤버 이름, 역할, 프로필 이미지, 블로그 경로, 소속 챕터 같은 정보는 RSS 설정과 성격이 다르기 때문에 둘을 분리했다.
flowchart TD
feeds["data/feeds.json<br/>RSS 수집 설정"]
members["data/members.json<br/>팀/멤버 정보"]
rss["src/lib/rss.ts"]
posts["Post[]<br/>화면용 글 데이터"]
feeds --> rss
members --> rss
rss --> posts
처음에는 하나의 파일에 다 넣는 편이 쉬워 보인다. 하지만 RSS 수집 설정과 팀 소개 데이터는 바뀌는 이유가 다르기 때문에 의미가 다른 데이터는 파일도 분리하는 편이 덜 헷갈린다.
RSS에는 없는 정보를 어떻게 붙일까
RSS에서 기대할 수 있는 정보는 제한적이다. 제목, 링크, 발행일, 요약 정도는 가져올 수 있지만, 이 글이 어느 챕터의 글인지는 RSS만 보고 알 수 없다.
그래서 글의 챕터는 멤버 정보에서 계산한다. 현재 프로젝트에는 챕터가 명시적인 타입으로 정의되어 있다.
export type ChapterKey = "ai" | "devops" | "be" | "fe";
export const CHAPTERS = [
{ key: "ai", label: "AI", teamName: "AI Chapter" },
{ key: "devops", label: "DevOps", teamName: "Devops Chapter" },
{ key: "be", label: "BE", teamName: "Backend Chapter" },
{ key: "fe", label: "FE", teamName: "Frontend Chapter" },
];
대문 사이트는 memberId를 기준으로 RSS 설정과 멤버 데이터를 연결한다.
const chapterByTeamName = new Map(
CHAPTERS.map((chapter) => [chapter.teamName, chapter.key]),
);
const memberIndex = new Map<string, MemberFeedMeta>();
for (const team of teams) {
const chapter = chapterByTeamName.get(team.name);
for (const member of team.members) {
memberIndex.set(member.id, { name: member.name, chapter });
}
}
이 인덱스를 사용하면 RSS에서 읽은 글에 author, authorId, chapter를 붙일 수 있다. 외부 피드에서 온 데이터를 화면에서 쓰기 좋은 내부 데이터로 한 번 바꾸는 셈이다.
이 변환이 있어야 UI가 단순해진다. LatestPosts는 RSS의 원본 구조를 몰라도 되고, 이미 정리된 Post 배열만 받아서 전체 글과 챕터별 글을 보여주면 된다.
대문은 서버에서 모으고, 브라우저에서는 골라 보여준다
홈 페이지는 서버 컴포넌트다. 최신 글과 팀 정보를 서버에서 준비한다.
export const revalidate = 3600;
export default async function Home() {
const [posts, teams] = await Promise.all([getLatestPosts(100), getTeams()]);
return (
<div className="mx-auto max-w-5xl px-6">
<Hero />
<LatestPosts posts={posts} />
<TeamSection teams={teams} />
</div>
);
}
RSS 수집은 브라우저보다 서버에서 처리하는 편이 낫다. 여러 외부 RSS를 브라우저에서 직접 요청하면 CORS나 로딩 상태를 신경 써야 하고, 사용자가 탭을 누르는 행위와 RSS 수집은 성격이 다르기 때문이다.
서버는 수집과 정렬을 맡고, revalidate = 3600으로 1시간 단위 갱신을 사용한다. 기술 블로그의 최신 글 목록은 초 단위 실시간성이 필요한 데이터가 아니므로 매 요청마다 모든 RSS를 새로 읽는 것보다 일정 주기로 다시 검증하는 쪽이 이 화면에는 맞다.
브라우저에서는 이미 받은 글 목록을 기준으로 탭과 페이지네이션만 처리한다. 현재 최신 포스트 영역은 All, AI, DEVOPS, BE, FE 탭을 제공하고, 서버가 넘긴 글 목록에서 현재 탭에 맞는 글만 골라 보여주는 구조다.
const filtered =
activeTab === "all"
? posts
: posts.filter((post) => post.chapter === activeTab);
const startIndex = (page - 1) * POSTS_PER_PAGE;
const visiblePosts = filtered.slice(startIndex, startIndex + POSTS_PER_PAGE);
대문 화면의 책임은 여기까지다. 블로그 글은 각자의 블로그에 있고, 대문은 최신 글을 모아서 보여주다가 사용자가 글을 누르면 개별 블로그로 이동시킨다. 느슨하지만 충분하다.
일부 RSS가 실패해도 대문은 살아 있어야 한다
RSS 기반 구조에는 당연한 약점이 있다. 개별 블로그는 대문 사이트가 완전히 통제할 수 없어서 어떤 블로그가 잠시 내려갈 수도 있고, RSS 형식이 깨질 수도 있다.
이때 한 명의 RSS 때문에 전체 대문이 죽으면 안 되므로 fetchFeed는 실패한 피드에 대해 빈 배열을 반환한다.
async function fetchFeed(
url: string,
memberId: string,
memberIndex: Map<string, MemberFeedMeta>,
): Promise<Post[]> {
try {
const feed = await parser.parseURL(url);
const member = memberIndex.get(memberId);
const authorName = member?.name ?? memberId;
return (feed.items ?? []).map((item) => ({
title: item.title ?? "Untitled",
link: item.link ?? "",
summary: item.contentSnippet?.slice(0, 200) ?? "",
author: authorName,
authorId: memberId,
chapter: member?.chapter,
pubDate: item.pubDate ?? new Date().toISOString(),
}));
} catch {
console.warn(`Failed to fetch RSS feed: ${url}`);
return [];
}
}
화려한 코드는 아니다. 그래도 이런 화면에서는 이런 방어가 더 필요하다. 팀블로그 대문은 여러 독립 블로그에 의존하므로 독립성을 허용했다면 일부 실패도 허용해야 한다. 특정 블로그의 RSS가 잠시 실패하면 그 사람의 글만 빠지고, 나머지 글은 계속 보이면 된다.
자유도를 주면 결과물도 달라진다
이 방식의 장점은 각 블로그가 서로 닮을 필요가 없다는 점이다.
회사에서 만든 하나의 에디터를 쓰면 글의 모양은 정돈되지만, 엔지니어가 직접 블로그를 만들며 얻는 경험은 줄어든다. 반대로 각자 블로그를 만들게 하면 디자인, 글 목록, 상세 화면, 기술 스택까지 결과물이 조금씩 달라질 수 있다.
그 차이를 일부러 허용했다. Aijinet Tech Blog의 대문은 기술연구소 전체의 입구, 챕터 소개, 최신 글 목록처럼 통일성이 필요한 부분을 담당하고, 각 글을 읽는 경험은 해당 엔지니어의 블로그로 넘긴다.
약간 느슨하지만 그 느슨함이 의도와 맞다. 팀 차원의 기록은 모으되, 개인의 구현 방식은 닫아두지 않는다.
AI 시대의 기술 블로그
요즘은 AI가 글 초안을 빠르게 만들어 준다. 블로그를 시작하는 비용은 분명히 낮아졌고, 제목을 뽑고 목차를 만들고 문장을 다듬는 일도 예전보다 쉬워졌다.
하지만 기술 블로그에서 필요한 모든 일이 자동화되는 것은 아니다.
AI는 초안을 도울 수 있다. 문장을 정리하고, 빠진 설명을 보완하고, 어색한 표현을 고치는 데 도움이 되지만 실제로 어떤 문제를 겪었는지, 어떤 선택지가 있었는지, 왜 그 선택을 했는지는 경험한 사람이 알고 있다.
기술 블로그는 결국 그 경험을 정리하는 일에 가깝다. 이 글도 마찬가지다. "Next.js로 RSS를 읽어 최신 글을 보여줬다"는 설명만 남기면 별로 남는 게 없고, 왜 CMS를 만들지 않았는지, 왜 각자 블로그를 직접 만들게 했는지, 왜 RSS만 최소 계약으로 두었는지가 더 오래 남는다.
AI가 글쓰기를 도와주는 시대에는 글의 표면이 비슷해지기 쉬워서 실제 판단의 흔적이 더 필요하다. 어떤 팀이 어떤 문제를 어떤 방식으로 풀었는지, 그 결정이 당시의 제약 안에서 왜 합리적이었는지 남겨야 한다.
팀블로그는 그런 기록을 쌓는 장소가 될 수 있다.
회고
Aijinet Tech Blog는 완성된 블로그 플랫폼이 아니다. 글쓰기 에디터도, 통합 CMS도 없다. 대신 기술연구소의 대문이 있고, 그 아래에 엔지니어별 독립 블로그가 붙으며, 대문은 RSS를 읽어 최신 글을 모으고 멤버와 챕터 정보를 붙인다.
구조만 보면 단순하다.
flowchart LR
engineer["엔지니어"]
blog["개별 블로그<br/>자유로운 구현"]
mdx["md / mdx 글"]
rss["rss.xml"]
landing["기술연구소 대문"]
reader["독자"]
engineer --> blog
blog --> mdx
blog --> rss
rss --> landing
landing --> reader
reader --> blog
하지만 이 단순함 안에 의도가 있다. 팀의 글을 한곳에 모으면서도 엔지니어가 자기 블로그를 직접 만들 자유도는 남기고 싶었고, 글을 쓰는 경험과 블로그를 만드는 경험을 분리하지 않고 둘 다 기술적 산출물로 보고 싶었다.
물론 한계도 있다. RSS 품질은 각 블로그 구현에 영향을 받고, 검색이나 태그 페이지도 더 보강할 수 있으며, 챕터 매핑도 규모가 커지면 더 명시적인 데이터 구조로 바꾸는 편이 나을 수 있다.
그래도 현재 단계에서는 이 정도의 구조가 맞다고 본다. 팀블로그는 꼭 하나의 시스템 안에 모든 글을 가둬야만 만들 수 있는 것은 아니며, 각자의 블로그를 그대로 두고 최소한의 계약으로 연결해도 팀의 기록은 만들어질 수 있다.
이 프로젝트는 오래된 팀블로그 개념을 다시 꺼내서, 엔지니어의 자유도와 지금의 배포 구조에 맞게 바꿔본 시도였다.