https://blog.cau-likelion.org/
멋사 중앙대만을 위한 블로그 피드를 제작해보았다.
nextjs와 typescipt를 사용해서 간단하게 rss를 파싱하는 방식으로 진행했다.
RSS란
Rich Site Summary또는 Really Simple Syndication의 약자로, 어떤 사이트에 새로운 컨텐츠가 올라왔을때, 해당 사이트를 방문하지 않고 컨텐츠를 이용하기 위한 방법이다.
블로그별 RSS주소
tistory : [blogName].tistory.com/rss
velog : v2.velog.io/rss@[velogID]
naver : rss.blog.naver.com/[naverID]
medium : medium.com/feed/@[MediumID]
이걸 활용하면 해당 유저의 id값, 또는 블로그 이름만 알아도 해당 블로그 글의 피드를 얻어올 수 있다.
rss-parser
그리고 놀랍게도 rss-parser라이브러리 또한 존재했다.
https://www.npmjs.com/package/rss-parser
parser.parseURL(url)을 통해 feed를 얻어올 수 있다.
해당 feed는 items로 구성되고, item하나는 title(글 제목), link(링크), content(글의 내용), isoDate(발행일)등으로 구성된다.
이걸 활용해서 다음과 같은 컴포넌트를 구성해 줄 것이다.
다른 것은 그냥 갖다쓰면되는데 content와 썸네일이 문제였다.
content 문제
1. html 태그
html태그가 그대로 노출되고, 줄바꿈 문자가 표기되는 문제가 있었다. replace메서드를 활용해서 없애주었다.
...
content: item.content.replace(/<[^>]*>?/g, '')
.replace(/\n/g, '')
.replace(/ /g, '')
.trim()
.replace(/\s+/g, ' ')
.slice(0, 300),
2. 광고문제
content에는 블로그 이용자가 광고를 삽입한 경우, 본문에 광고 스크립트가 표기되는 문제가 있었다.
광고스크립트 div의 클래스는 모두 .revenue_unit_wrap으로 시작했는데, 처음에는 이걸 정규식 필터링으로 해결해보고자 했었다.
하지만, <div></div>의 구성이 중첩되어 어디까지가 정확히 .revenue_unit_wrap의 div가 닫히는 구간인지 판단하기 힘든 부분이 있었고, 그냥 DOM라이브러리를 찾아보자는 결론에 이르렀다.
다행이 JSDOM이라는 npm 라이브러리가 있었고, 이를 활용해서 광고 문제를 해결했다.
https://www.npmjs.com/package/jsdom
jsdom은 해당 content를 dom형태로 만들 수 있고, jsdom인스턴스.window.document를 통해 얻은 document로 DOM API문법을 사용할 수 있다.
이를 활용해 아래와 같이 removeAdd를 만들어주었다.
const removeAdd = (content: string | undefined) => {
if (!content) return '';
const dom = new JSDOM(content);
const document = dom.window.document;
const revenueUnitWraps = document.querySelectorAll('.revenue_unit_wrap');
revenueUnitWraps.forEach((revenueUnitWrap) => {
revenueUnitWrap.remove();
});
const outputHTML = document.documentElement.outerHTML;
return outputHTML;
};
3. medium rss의 문제
웃기게도 medium은 자기 혼자 content태그가 아니라 content:encoded를 사용한다.
이를 해결하기 위해 decideConten함수로 content내용을 결정해주었다.
const decideContent = (item: feed) => {
if (item.content) return item.content;
return item['content:encoded'];
};
썸네일 문제
문제는 썸네일이었다. 본문에서 긁어왔어야 했는데, 이부분은 처음 만나는 img태그를 긁어오도록 정규식을 짜주었다.
const getThumbnail = (content: string | undefined) => {
const regex = /<img\s+src=(?:(['"])(.*?)\1|([^'"\s]+))/g;
if (content) {
const result = content.match(regex);
if (result) return result[0].split(/src=/)[1].split(/\'|\"/)[1];
return null;
}
return null;
};
전체코드
참고로 blogList는 [{name:"이름", blog:"rss주소"}, ...]의 형태다.
export const getRss = async () => {
const parser = new Parser();
return await Promise.all(
blogList.map(async ({ name, blog }) => {
const feed = await parser.parseURL(blog);
const result = feed.items.map((item) => {
const content = decideContent(item as feed);
return {
title: item.title,
writer: name,
link: item.link,
content: removeAdd(content)
.replace(/<[^>]*>?/g, '')
.replace(/\n/g, '')
.replace(/ /g, '')
.trim()
.replace(/\s+/g, ' ')
.slice(0, 300),
thumbnail: getThumbnail(content),
date: item.isoDate,
};
});
return result as unknown as feed[];
})
);
};
속도의 문제
이렇게 해서 SSR로 배포하자 생긴 문제가 바로 렌더링 문제였다.
위 글을 참고하면 된다.