Skip to content

Commit

Permalink
refactor(fe): optimize QuestionList, QuestionSection rendering perfor…
Browse files Browse the repository at this point in the history
…mance (#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
  • Loading branch information
cjeongmin authored Jan 23, 2025
1 parent cedd69f commit 963bf8d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 43 deletions.
32 changes: 32 additions & 0 deletions apps/client/src/shared/model/deep-equal/deep-equal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const isPrimitive = (value: unknown): boolean => {
return value === null || (typeof value !== 'object' && typeof value !== 'function');
};

const areArraysEqual = (lhs: unknown[], rhs: unknown[]): boolean => {
if (lhs.length !== rhs.length) return false;
return lhs.every((item, index) => deepEqual(item, rhs[index]));
};

const areObjectsEqual = (lhs: Record<string, unknown>, rhs: Record<string, unknown>): boolean => {
const lhsKeys = Object.keys(lhs);
const rhsKeys = Object.keys(rhs);

if (lhsKeys.length !== rhsKeys.length) return false;

return lhsKeys.every((key) => deepEqual(lhs[key], rhs[key]));
};

export const deepEqual = (lhs: unknown, rhs: unknown): boolean => {
if (lhs === rhs) return true;
if (isPrimitive(lhs) || isPrimitive(rhs)) return false;

if (Array.isArray(lhs) && Array.isArray(rhs)) {
return areArraysEqual(lhs, rhs);
}

if (typeof lhs === 'object' && typeof rhs === 'object') {
return areObjectsEqual(lhs as Record<string, unknown>, rhs as Record<string, unknown>);
}

return false;
};
1 change: 1 addition & 0 deletions apps/client/src/shared/model/deep-equal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './deep-equal';
97 changes: 55 additions & 42 deletions apps/client/src/widgets/question-list/ui/QuestionList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isAxiosError } from 'axios';
import { motion } from 'motion/react';
import { useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { GrValidate } from 'react-icons/gr';
import { IoClose, IoShareSocialOutline } from 'react-icons/io5';
import { useShallow } from 'zustand/react/shallow';
Expand All @@ -13,8 +13,9 @@ import { SessionParticipantsModal } from '@/features/get-session-users';
import { useSocket } from '@/features/socket';
import { postSessionTerminate, SessionTerminateModal } from '@/features/terminate-session';

import { useSessionStore } from '@/entities/session';
import { Question, useSessionStore } from '@/entities/session';

import { deepEqual } from '@/shared/model/deep-equal';
import { Button } from '@/shared/ui/button';
import { useModal } from '@/shared/ui/modal';
import { useToastStore } from '@/shared/ui/toast';
Expand Down Expand Up @@ -78,37 +79,6 @@ function QuestionList() {

const buttonRef = useRef<HTMLButtonElement>(null);

const sections = useMemo(
() => [
{
title: '고정된 질문',
initialOpen: true,
questions: questions
.filter((question) => question.pinned && !question.closed)
.sort((a, b) => b.likesCount - a.likesCount),
},
{
title: '질문',
initialOpen: true,
questions: questions
.filter((question) => !question.pinned && !question.closed)
.sort((a, b) => b.likesCount - a.likesCount),
},
{
title: '답변 완료된 질문',
initialOpen: false,
questions: questions
.filter((question) => question.closed)
.sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.likesCount - a.likesCount;
}),
},
],
[questions],
);

const sessionButtons = useMemo(
() => [
{
Expand Down Expand Up @@ -164,6 +134,40 @@ function QuestionList() {
[sessionId, addToast, openSessionParticipantsModal, openSessionTerminateModal],
);

const [pinnedQuestions, setPinnedQuestions] = useState<Question[]>([]);
const [unpinnedQuestions, setUnpinnedQuestions] = useState<Question[]>([]);
const [closedQuestions, setClosedQuestions] = useState<Question[]>([]);

useEffect(() => {
const updatedPinnedQuestions = questions
.filter((question) => question.pinned && !question.closed)
.sort((a, b) => b.likesCount - a.likesCount);

const updatedUnpinnedQuestions = questions
.filter((question) => !question.pinned && !question.closed)
.sort((a, b) => b.likesCount - a.likesCount);

const updatedClosedQuestions = questions
.filter((question) => question.closed)
.sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return b.likesCount - a.likesCount;
});

if (!deepEqual(updatedPinnedQuestions, pinnedQuestions)) {
setPinnedQuestions(updatedPinnedQuestions);
}

if (!deepEqual(updatedUnpinnedQuestions, unpinnedQuestions)) {
setUnpinnedQuestions(updatedUnpinnedQuestions);
}

if (!deepEqual(updatedClosedQuestions, closedQuestions)) {
setClosedQuestions(updatedClosedQuestions);
}
}, [questions, pinnedQuestions, unpinnedQuestions, closedQuestions]);

return (
<div className='inline-flex h-full w-4/5 flex-grow flex-col items-center justify-start rounded-lg bg-white shadow'>
<div className='inline-flex h-[54px] w-full items-center justify-between border-b border-gray-200 px-8 py-2'>
Expand Down Expand Up @@ -210,15 +214,24 @@ function QuestionList() {
</div>
) : (
<motion.div className='inline-flex h-full w-full flex-col items-start justify-start gap-4 overflow-y-auto px-8 py-4'>
{sections.map((section) => (
<QuestionSection
key={section.title}
title={section.title}
initialOpen={section.initialOpen}
questions={section.questions}
onQuestionSelect={setSelectedQuestionId}
/>
))}
<QuestionSection
title='고정된 질문'
initialOpen={true}
questions={pinnedQuestions}
onQuestionSelect={setSelectedQuestionId}
/>
<QuestionSection
title='질문'
initialOpen={true}
questions={unpinnedQuestions}
onQuestionSelect={setSelectedQuestionId}
/>
<QuestionSection
title='답변 완료된 질문'
initialOpen={false}
questions={closedQuestions}
onQuestionSelect={setSelectedQuestionId}
/>
</motion.div>
)}
{CreateQuestion}
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/widgets/question-list/ui/QuestionSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AnimatePresence, motion, Variants } from 'motion/react';
import { useState } from 'react';
import React from 'react';

import QuestionDivider from '@/widgets/question-list/ui/QuestionDivider';
import QuestionItem from '@/widgets/question-list/ui/QuestionItem';
Expand Down Expand Up @@ -86,4 +87,4 @@ function QuestionSection({ title, questions, initialOpen, onQuestionSelect }: Qu
);
}

export default QuestionSection;
export default React.memo(QuestionSection);

0 comments on commit 963bf8d

Please sign in to comment.