INTRO
머릿속으로 어떻게 해야 이 기능을 동작하게 할 수 있을까 고민했다. 일단 제목 전체가 내용에 있는 텍스트와 비교해서 같은게 있는지 찾고 같으면 링크를 생성하고 그걸 클릭하면 해당 제목의 디테일 페이지로 이동해야 한다. 이정도로 생각을 정리하고 나서 코딩을 시작했다. 조금 더 생각하고 코드를 작성해야 했다..
시도 1
// 포스트 내용에서 다른 포스트 제목을 찾아 링크로 변환
const renderedContent = post.content.split(' ').map((word, index) => {
const matchingPost = posts.find(p => p.title === word);
if (matchingPost) {
return <Link key={index} to={`/postDetail/${matchingPost.id}`}>{word}</Link>;
}
return word + ' ';
});
이 시도는 공백을 기준으로 내용에 해당하는 텍스트를 분리해서 제목과 일치하는지 검사하는 로직이었다. 당연한 말이겠지만 작동하지 않았다. 여기에서 간과했던 것은 제목이 만약 '가수'라면 내용에는 '가수' 만 있지 않다는 것이었다. '가수이다.'라고 서술형으로 써져있으면 당연하게도 컴퓨터는 다른 거라고 생각한다..^^ 한글이 영어보다 처리하기 어렵다는 이야기를 이렇게 또 이해해버렸다.
시도 2
// 포스트 내용에서 다른 포스트의 제목을 식별하고, 클릭 가능한 요소로 변환
const processedContent = posts.reduce((acc, { id, title }) => {
// 제목이 포함된 경우, 링크를 추가
if (post.content.includes(title)) {
const replacedText = `<span onClick={() => navigate('/postDetail/${id}')}" style="color: blue; cursor: pointer;">${title}</span>`;
acc = acc.split(title).join(replacedText);
}
return acc;
}, post.content);
제목과 내용이 같은지 확인하는게 아니라 포함하고 있는지 확인하는 걸로 로직을 바꿨다. 이제 제목이 있다면 링크는 잘 생성된다. 근데 두가지 문제가 있었다. 첫번째는 dangerouslySetInnerHTML로 생성된 HTML 내에서는 Link 컴포넌트를 직접 사용할 수 없기 때문에 페이지 이동이 안된다는 것이었고, 두번째는 dangerouslySetInnerHTML가 XSS공격에 매우 취약하다는 것이었다.제목과 내용이 같은지 확인하는게 아니라 포함하고 있는지 확인하는 걸로 로직을 바꿨다. 이제 제목이 있다면 링크는 잘 생성된다. 근데 두가지 문제가 있었다. 첫번째는 dangerouslySetInnerHTML로 생성된 HTML 내에서는 Link 컴포넌트를 직접 사용할 수 없기 때문에 페이지 이동이 안된다는 것이었고, 두번째는 dangerouslySetInnerHTML가 XSS공격에 매우 취약하다는 것이었다.
시도 3
// 제목을 키로, 포스트 ID를 값으로 하는 맵 생성
const titleToIdMap = posts.reduce((acc, cur) => {
acc[cur.title] = cur.id;
return acc;
}, {});
// 포스트 내용을 React 컴포넌트 배열로 변환
const contentComponents = post.content.split(/\s+/).map((word, index) => {
let match = null;
// 제목을 키로 사용하여 직접 매칭 여부 확인
const title = Object.keys(titleToIdMap).find(title => word.includes(title));
if (title) {
// 제목과 일치하는 경우, Link 컴포넌트를 반환
return (
<React.Fragment key={index}>
<Link to={`/postDetail/${titleToIdMap[title]}`}>{word}</Link>{' '}
</React.Fragment>
);
} else {
// 일치하지 않는 경우, 단어를 그대로 반환
return `${word} `;
}
});
이제 이 코드는 자동으로 링크를 걸고 클릭하면 잘 이동한다. 하지만 이 코드로 테스트를 해보니 문제를 발견했다. 제목 중간에 띄어쓰기가 있으면 링크가 안걸리는 것이다. 그리고 단어에 조사가 같이 붙어있으면 조사까지 링크가 걸린다. 그래서 이 문제를 해결하기 위해서 결국 맨처음에 생각한 로직으로 돌아가게 되었다. 이제 글자 하나하나 분리해서 검사하고 링크를 달기로 했다. 제목을 배열로 담아서 마지막까지 일치하는지 확인하면 띄어쓰기도 해결 될 거라고 생각했다.
시도 4
// 전체 내용을 담을 배열
let contentWithLinks: React.ReactNode[] = [];
let lastIndex = 0; // 마지막으로 처리한 인덱스
// 제목이 포함된 위치를 찾아서 링크를 생성하는 함수
posts.forEach(({ id, title }) => {
let titleIndex = post.content.indexOf(title, lastIndex);
while (titleIndex !== -1) {
// 제목 이전의 텍스트 추가
if (titleIndex > lastIndex) {
contentWithLinks.push(post.content.substring(lastIndex, titleIndex));
}
// 제목을 Link로 변환
contentWithLinks.push(
<Link key={`${id}-${titleIndex}`} to={`/postDetail/${id}`}>
{title}
</Link>
);
// 조사 처리: 제목 바로 뒤의 조사를 식별하기 위한 정규식
const postTitleRegex = new RegExp(`^(${title})([가-힣]{1,2})`, 'g');
const matches = postTitleRegex.exec(post.content.substring(titleIndex));
if (matches && matches[2]) {
// 조사가 식별된 경우, 조사도 텍스트로 추가
contentWithLinks.push(matches[2]);
}
// 다음 검색 시작 위치 업데이트
lastIndex = titleIndex + title.length + (matches && matches[2] ? matches[2].length : 0);
titleIndex = post.content.indexOf(title, lastIndex); // 다음 제목 위치 검색
}
});
// 마지막 제목 이후의 내용 추가
if (lastIndex < post.content.length) {
contentWithLinks.push(post.content.substring(lastIndex));
}
이제 잘 작동한다. 코드가 너무 길어진 것 같은데 그래도 해당 단어에만 링크가 잘 걸린다.
배포 후 디버깅
그렇게 기능 구현을 모두 잘 마쳤다고 생각하고 며칠 뒤에 배포 페이지에 들어갔다. 이미 과제는 제출한 상태..
오류를 발견했다..^_ㅠ 내용에 링크가 걸려야 할 제목이 여러개라면 링크가 맨 마지막 요소에만 걸리는 것이다. 확인하고 나는 멘붕이 왔다. 하지만 어쩔 수 없지. 지금이라도 수정하기로 했다.
// 원본 content에서 모든 제목의 위치 정보 수집
posts.forEach(({ id, title }) => {
let index = post.content.indexOf(title);
while (index !== -1) {
titlesInfo.push({ index, length: title.length, id, title });
index = post.content.indexOf(title, index + title.length);
}
});
// 위치 정보를 바탕으로 contentWithLinks 배열 생성
const contentWithLinks = [];
let lastIndex = 0;
// 위치 정보를 기준으로 정렬
titlesInfo.sort((a, b) => a.index - b.index);
titlesInfo.forEach(({ id, title, index, length }) => {
// 이전 제목과 현재 제목 사이의 텍스트 추가
if (index > lastIndex) {
contentWithLinks.push(post.content.substring(lastIndex, index));
}
// 현재 제목에 대한 링크 생성
contentWithLinks.push(
<Link key={`${id}-${index}`} to={`/postDetail/${id}`}>
{title}
</Link>,
);
lastIndex = index + length;
});
// 마지막 제목 이후의 텍스트 추가
if (lastIndex < post.content.length) {
contentWithLinks.push(post.content.substring(lastIndex));
}
이제 정말 정상적으로 동작한다. 진짜 트러블 슈팅 끝..!
'2024 > 프로젝트' 카테고리의 다른 글
[Wiki Page] typescript에서 만난 오류 디버깅 (0) | 2024.04.07 |
---|---|
[WriteMate] 디스코드 봇 명령어 설정하기 (1) | 2024.03.23 |
[Mudig] 불필요한 리렌더링 방지로 성능 최적화 하는 방법 (1) | 2024.03.19 |
[WriteMate] 디스코드 봇 만들기 (0) | 2024.03.12 |
[Mudig] 리팩토링 중 마주친 Recoil Hook 무엇이 좋을까? (0) | 2024.03.01 |