Skip to content

Commit 963bf8d

Browse files
authored
refactor(fe): optimize QuestionList, QuestionSection rendering performance (#42)
* feat: add deep equality utility function * refactor: refactor QuestionList to use useEffect for question state management * refactor: optimize QuestionSection component with React.memo for performance * refactor: remove comment * refactor: remove console logs for question updates in QuestionList
1 parent cedd69f commit 963bf8d

File tree

4 files changed

+90
-43
lines changed

4 files changed

+90
-43
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const isPrimitive = (value: unknown): boolean => {
2+
return value === null || (typeof value !== 'object' && typeof value !== 'function');
3+
};
4+
5+
const areArraysEqual = (lhs: unknown[], rhs: unknown[]): boolean => {
6+
if (lhs.length !== rhs.length) return false;
7+
return lhs.every((item, index) => deepEqual(item, rhs[index]));
8+
};
9+
10+
const areObjectsEqual = (lhs: Record<string, unknown>, rhs: Record<string, unknown>): boolean => {
11+
const lhsKeys = Object.keys(lhs);
12+
const rhsKeys = Object.keys(rhs);
13+
14+
if (lhsKeys.length !== rhsKeys.length) return false;
15+
16+
return lhsKeys.every((key) => deepEqual(lhs[key], rhs[key]));
17+
};
18+
19+
export const deepEqual = (lhs: unknown, rhs: unknown): boolean => {
20+
if (lhs === rhs) return true;
21+
if (isPrimitive(lhs) || isPrimitive(rhs)) return false;
22+
23+
if (Array.isArray(lhs) && Array.isArray(rhs)) {
24+
return areArraysEqual(lhs, rhs);
25+
}
26+
27+
if (typeof lhs === 'object' && typeof rhs === 'object') {
28+
return areObjectsEqual(lhs as Record<string, unknown>, rhs as Record<string, unknown>);
29+
}
30+
31+
return false;
32+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './deep-equal';

apps/client/src/widgets/question-list/ui/QuestionList.tsx

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isAxiosError } from 'axios';
22
import { motion } from 'motion/react';
3-
import { useMemo, useRef, useState } from 'react';
3+
import { useEffect, useMemo, useRef, useState } from 'react';
44
import { GrValidate } from 'react-icons/gr';
55
import { IoClose, IoShareSocialOutline } from 'react-icons/io5';
66
import { useShallow } from 'zustand/react/shallow';
@@ -13,8 +13,9 @@ import { SessionParticipantsModal } from '@/features/get-session-users';
1313
import { useSocket } from '@/features/socket';
1414
import { postSessionTerminate, SessionTerminateModal } from '@/features/terminate-session';
1515

16-
import { useSessionStore } from '@/entities/session';
16+
import { Question, useSessionStore } from '@/entities/session';
1717

18+
import { deepEqual } from '@/shared/model/deep-equal';
1819
import { Button } from '@/shared/ui/button';
1920
import { useModal } from '@/shared/ui/modal';
2021
import { useToastStore } from '@/shared/ui/toast';
@@ -78,37 +79,6 @@ function QuestionList() {
7879

7980
const buttonRef = useRef<HTMLButtonElement>(null);
8081

81-
const sections = useMemo(
82-
() => [
83-
{
84-
title: '고정된 질문',
85-
initialOpen: true,
86-
questions: questions
87-
.filter((question) => question.pinned && !question.closed)
88-
.sort((a, b) => b.likesCount - a.likesCount),
89-
},
90-
{
91-
title: '질문',
92-
initialOpen: true,
93-
questions: questions
94-
.filter((question) => !question.pinned && !question.closed)
95-
.sort((a, b) => b.likesCount - a.likesCount),
96-
},
97-
{
98-
title: '답변 완료된 질문',
99-
initialOpen: false,
100-
questions: questions
101-
.filter((question) => question.closed)
102-
.sort((a, b) => {
103-
if (a.pinned && !b.pinned) return -1;
104-
if (!a.pinned && b.pinned) return 1;
105-
return b.likesCount - a.likesCount;
106-
}),
107-
},
108-
],
109-
[questions],
110-
);
111-
11282
const sessionButtons = useMemo(
11383
() => [
11484
{
@@ -164,6 +134,40 @@ function QuestionList() {
164134
[sessionId, addToast, openSessionParticipantsModal, openSessionTerminateModal],
165135
);
166136

137+
const [pinnedQuestions, setPinnedQuestions] = useState<Question[]>([]);
138+
const [unpinnedQuestions, setUnpinnedQuestions] = useState<Question[]>([]);
139+
const [closedQuestions, setClosedQuestions] = useState<Question[]>([]);
140+
141+
useEffect(() => {
142+
const updatedPinnedQuestions = questions
143+
.filter((question) => question.pinned && !question.closed)
144+
.sort((a, b) => b.likesCount - a.likesCount);
145+
146+
const updatedUnpinnedQuestions = questions
147+
.filter((question) => !question.pinned && !question.closed)
148+
.sort((a, b) => b.likesCount - a.likesCount);
149+
150+
const updatedClosedQuestions = questions
151+
.filter((question) => question.closed)
152+
.sort((a, b) => {
153+
if (a.pinned && !b.pinned) return -1;
154+
if (!a.pinned && b.pinned) return 1;
155+
return b.likesCount - a.likesCount;
156+
});
157+
158+
if (!deepEqual(updatedPinnedQuestions, pinnedQuestions)) {
159+
setPinnedQuestions(updatedPinnedQuestions);
160+
}
161+
162+
if (!deepEqual(updatedUnpinnedQuestions, unpinnedQuestions)) {
163+
setUnpinnedQuestions(updatedUnpinnedQuestions);
164+
}
165+
166+
if (!deepEqual(updatedClosedQuestions, closedQuestions)) {
167+
setClosedQuestions(updatedClosedQuestions);
168+
}
169+
}, [questions, pinnedQuestions, unpinnedQuestions, closedQuestions]);
170+
167171
return (
168172
<div className='inline-flex h-full w-4/5 flex-grow flex-col items-center justify-start rounded-lg bg-white shadow'>
169173
<div className='inline-flex h-[54px] w-full items-center justify-between border-b border-gray-200 px-8 py-2'>
@@ -210,15 +214,24 @@ function QuestionList() {
210214
</div>
211215
) : (
212216
<motion.div className='inline-flex h-full w-full flex-col items-start justify-start gap-4 overflow-y-auto px-8 py-4'>
213-
{sections.map((section) => (
214-
<QuestionSection
215-
key={section.title}
216-
title={section.title}
217-
initialOpen={section.initialOpen}
218-
questions={section.questions}
219-
onQuestionSelect={setSelectedQuestionId}
220-
/>
221-
))}
217+
<QuestionSection
218+
title='고정된 질문'
219+
initialOpen={true}
220+
questions={pinnedQuestions}
221+
onQuestionSelect={setSelectedQuestionId}
222+
/>
223+
<QuestionSection
224+
title='질문'
225+
initialOpen={true}
226+
questions={unpinnedQuestions}
227+
onQuestionSelect={setSelectedQuestionId}
228+
/>
229+
<QuestionSection
230+
title='답변 완료된 질문'
231+
initialOpen={false}
232+
questions={closedQuestions}
233+
onQuestionSelect={setSelectedQuestionId}
234+
/>
222235
</motion.div>
223236
)}
224237
{CreateQuestion}

apps/client/src/widgets/question-list/ui/QuestionSection.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AnimatePresence, motion, Variants } from 'motion/react';
22
import { useState } from 'react';
3+
import React from 'react';
34

45
import QuestionDivider from '@/widgets/question-list/ui/QuestionDivider';
56
import QuestionItem from '@/widgets/question-list/ui/QuestionItem';
@@ -86,4 +87,4 @@ function QuestionSection({ title, questions, initialOpen, onQuestionSelect }: Qu
8687
);
8788
}
8889

89-
export default QuestionSection;
90+
export default React.memo(QuestionSection);

0 commit comments

Comments
 (0)