{localMessageList?.map((message: ResGetMessageListDataType) => (
<div className={`msg ${message.role === 'assistant' ? 'bot-msg-wrap' : 'user-msg-wrap'}`} key={message.id}>
{message.content[0]?.text?.value && (
<div className={message.role === 'assistant' ? 'bot-msg' : 'user-msg'}>
<div className={`msg-box ${message.role === 'user' ? 'msg-box-user' : ''}`}>
<p>
{parse(
handleTextMessage(message.content[0].text.value,message.attachments?.[0]?.file_id),
)}
</p>
</div>
</div>
)}
</div>
))}
Problem
handleTextMessage(message.content[0].text.value,message.attachments?.[0]?.file_id) 이렇게 handleTextMessage에 메시지 값이랑 메시지 파일 아이디를 넣어서 state로 fileId를 저장하고 훅으로 저장된 값을 보내어 업데이트된 fileURL을 가져와서
const { data: fileURL } = useGetFileDownloadLink({ file_id: fileId });
handleTextMessage에 다음과 같이 넣어주었다.
const handleTextMessage = (text: string, fileId?: string) => {
// 다운받을만한 엑셀 파일, pdf 파일 등이 있을 경우 fileId 저장
if (fileId) {
setFileId(fileId);
}
const fileURLRegex = /\[([^\]]+)\]\((sandbox:[^\s)]+)\)/g;
text = text.replace(fileURLRegex, (match, label, url) => {
const newUrl = fileURL?.url;
return `<a href="${newUrl}">${label}</a>`;
});
return text;
};
근데 다음과 같은 에러가 발생했다.
The error "Too many re-renders. React limits the number of renders to prevent an infinite loop." often occurs when a component continuously re-renders, usually due to an issue in the useEffect hooks or when a state update triggers another render immediately
컴포넌트가 재렌더링되는 문제가 발생한 것.
이유는 setFileId가 fileId 상태를 변경하면서 state 업데이트로 인해 컴포넌트가 재렌더링 되고 재렌더링으로 컴포넌트 안에 있는 handleTextMessage가 실행되면서 setFileId가 fileId가 변경되지 않았음에도 불구하고 다시 저장하게 되는 문제,
즉, handleTextMessage 안에 있는 setFileId -> fileId state 저장 -> state 업데이트로 컴포넌트 재렌더링 -> 재렌더링으로 함수 다시 실행 -> handleTextMessage 안에 있는 setFileId -> fileId state 저장 ......
이렇게 무한루프가 되어서 생긴 문제였다.
Solution1
우선 handleTextMessage 함수가 불필요하게 컴포넌트 안에 있어 컴포넌트 밖으로 뺐다.
그리고 useEffect를 사용하여 fileId가 변경될 때 useGetFileDownloadLink 훅의 refetch함수를 사용하여 새로운 fileId에 대한 fileURL을 가져왔다. 따라서 불필요하게 컴포넌트가 재렌더링이 되지 않아 해당 문제를 해결했다.
하지만
이전 메시지의 fileURL이 최신의 fileURL에 덮어져. 다른 파일이 다운로드 되어야 하는데 모두 같은 fileURL 주소를 바라보고 있어 같은 파일이 다운로드 되는 문제가 발생하였다.
Final Solution
다음과 같은 훅을
import { useQuery } from '@tanstack/react-query';
import axiosInstance from '../../../api/axiosInstance';
export const FILE_DOWNLOAD_LINK = ['retrieve_run'];
interface ReqGetFileDownloadLink {
file_id: string;
}
export interface ResGetFileDownloadLinkDataType {
object: string;
url: string;
}
const getFileDownloadLink = async ({ file_id }: ReqGetFileDownloadLink) => {
try {
const response = await axiosInstance.get(`/${file_id}/download_link`);
return response.data;
} catch (err) {
console.log('err ::: ', err);
}
};
export const useGetFileDownloadLink = ({ file_id }: ReqGetFileDownloadLink) => {
return useQuery({
queryKey: [FILE_DOWNLOAD_LINK, file_id],
queryFn: () => getFileDownloadLink({ file_id }),
enabled: !!file_id,
});
};
아래와 같이 변경했다.
import { useQueries } from '@tanstack/react-query';
import axiosInstance from '../../../api/axiosInstance';
export const FILE_DOWNLOAD_LINK = ['retrieve_run'];
interface ReqGetFileDownloadLink {
file_id: string;
}
export interface ResGetFileDownloadLinkDataType {
object: string;
url: string;
}
const getFileDownloadLink = async ({ file_id }: ReqGetFileDownloadLink) => {
try {
const response = await axiosInstance.get(`/${file_id}/download_link`);
return response.data;
} catch (err) {
console.log('err ::: ', err);
}
};
export const useGetFileDownloadLinks = (fileIds: string[]) => {
const queries = fileIds.map(file_id => ({
queryKey: ['retrieve_run', file_id],
queryFn: () => getFileDownloadLink({ file_id }),
enabled: !!file_id,
}));
return useQueries({ queries });
};
이 둘의 차이는 useQuery와 useQueries의 차이점에서 있다.
useQuery는 단일 쿼리를 처리하는 데 사용된다. 일반적으로 하나의 데이터를 가져와 컴포넌트에 전달할 때 사용
useQueries는 여러 쿼리를 병렬로 처리하는 데 사용된다. 여러 데이터 소스에서 데이터를 동시에 가져와야 할 때 유용하다. 각 쿼리마다 별도의 상태(isLoading, error, data 등)를 가진다.
함수에서도 변경사항을 적용하였다.
변경 전
const [fileId, setFileId] = useState<string>('');
useEffect(() => {
if (fileId) {
refetchFileURL();
}
}, [fileId, refetchFileURL]);
변경 후
const fileIds = (localMessageList || []).flatMap(message =>
message.attachments ? message.attachments.map(attachment => attachment.file_id) : [],
);
const fileLinksQueries = useGetFileDownloadLinks(fileIds);
attachment의 file_id를 매핑하여 배열로 만들어 준다.
< 메시지 리스트 예시 값 >
const localMessageList = [
{
id: 'msg1',
attachments: [
{ file_id: 'file1' },
{ file_id: 'file2' }
]
},
{
id: 'msg2',
attachments: [
{ file_id: 'file3' }
]
},
{
id: 'msg3',
attachments: [] // 빈 첨부파일 목록
},
{
id: 'msg4' // 첨부파일 없음
}
];
추가로
const fileURLs = fileLinksQueries.reduce(
(acc, cur, index) => {
if (cur.data) {
const fileId = fileIds[index];
acc[fileId] = cur.data;
}
return acc;
},
{} as { [key: string]: FileURLData },
);
useGetFileDownloadLinks훅을 통해 얻은 여러 fileURL 들을 담은 훅 fileLinksQueries를 가져와서 만약 현재 데이터가 존재한다면 해당 객체를 추가.
그리고 현재 fileId값을 가지고 있는 배열 fileIds 배열에서 현재 인덱스에 대항하는 파일 Id를 가져옴
그리고 key값을 fileId로 만들고 value에 url을 넣는다.
<p>
{parse(handleTextMessage(message.content[0].text.value, fileURLs[message.attachments?.[0]?.file_id]))}
</p>
이렇게 바로바로 fileId를 가져와 해당 fileURLs에 맞는 fileId에 해당하는 url을 가져오는 걸로 에러 해결!
'Problem&Solution' 카테고리의 다른 글
IME과 textarea enter처리 (0) | 2024.07.09 |
---|---|
react-query, retetchOnWindowFocus (0) | 2024.06.26 |
react hook의 호출 규칙 (0) | 2024.05.08 |
객체 비교에서 한 실수 (1) | 2024.05.02 |
uuid를 map의 key값으로 사용하고 생긴 문제점 (0) | 2024.05.02 |