Skip to content

Commit 37f4a13

Browse files
authored
refactor(fe): optimize state access (#22)
* refactor: optimize state access * refactor: improve session state access in SocketProvider
1 parent 53f1b98 commit 37f4a13

File tree

5 files changed

+122
-101
lines changed

5 files changed

+122
-101
lines changed

apps/client/src/components/qna/QuestionItem.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@ interface QuestionItemProps {
3030
function QuestionItem({ question, onQuestionSelect }: QuestionItemProps) {
3131
const navigate = useNavigate();
3232

33-
const { addToast } = useToastStore();
34-
3533
const { Modal: CreateQuestion, openModal: openCreateQuestionModal } = useModal(
3634
<CreateQuestionModal question={question} />,
3735
);
3836

39-
const { sessionToken, sessionId, isHost, expired, removeQuestion, updateQuestion, setFromDetail } = useSessionStore();
37+
const sessionToken = useSessionStore((state) => state.sessionToken);
38+
const sessionId = useSessionStore((state) => state.sessionId);
39+
const isHost = useSessionStore((state) => state.isHost);
40+
const expired = useSessionStore((state) => state.expired);
41+
const removeQuestion = useSessionStore((state) => state.removeQuestion);
42+
const updateQuestion = useSessionStore((state) => state.updateQuestion);
43+
const setFromDetail = useSessionStore((state) => state.setFromDetail);
44+
45+
const addToast = useToastStore((state) => state.addToast);
4046

4147
const handleSelectQuestionId = () => {
4248
if (!sessionId) return;

apps/client/src/components/qna/QuestionList.tsx

Lines changed: 93 additions & 81 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 { useRef, useState } from 'react';
3+
import { useMemo, useRef, useState } from 'react';
44
import { GrValidate } from 'react-icons/gr';
55
import { IoClose, IoShareSocialOutline } from 'react-icons/io5';
66

@@ -15,12 +15,18 @@ import QuestionSection from '@/components/qna/QuestionSection';
1515
import SessionSettingsDropdown from '@/components/qna/SessionSettingsDropdown';
1616

1717
function QuestionList() {
18-
const { isHost, expired, questions, sessionId, sessionTitle, sessionToken, setExpired, setSelectedQuestionId } =
19-
useSessionStore();
18+
const isHost = useSessionStore((state) => state.isHost);
19+
const expired = useSessionStore((state) => state.expired);
20+
const questions = useSessionStore((state) => state.questions);
21+
const sessionId = useSessionStore((state) => state.sessionId);
22+
const sessionTitle = useSessionStore((state) => state.sessionTitle);
23+
const sessionToken = useSessionStore((state) => state.sessionToken);
24+
const setExpired = useSessionStore((state) => state.setExpired);
25+
const setSelectedQuestionId = useSessionStore((state) => state.setSelectedQuestionId);
2026

21-
const socket = useSocket();
27+
const addToast = useToastStore((state) => state.addToast);
2228

23-
const { addToast } = useToastStore();
29+
const socket = useSocket();
2430

2531
const { Modal: CreateQuestion, openModal: openCreateQuestionModal } = useModal(<CreateQuestionModal />);
2632

@@ -62,83 +68,89 @@ function QuestionList() {
6268

6369
const buttonRef = useRef<HTMLButtonElement>(null);
6470

65-
const sections = [
66-
{
67-
title: '고정된 질문',
68-
initialOpen: true,
69-
questions: questions
70-
.filter((question) => question.pinned && !question.closed)
71-
.sort((a, b) => b.likesCount - a.likesCount),
72-
},
73-
{
74-
title: '질문',
75-
initialOpen: true,
76-
questions: questions
77-
.filter((question) => !question.pinned && !question.closed)
78-
.sort((a, b) => b.likesCount - a.likesCount),
79-
},
80-
{
81-
title: '답변 완료된 질문',
82-
initialOpen: false,
83-
questions: questions
84-
.filter((question) => question.closed)
85-
.sort((a, b) => {
86-
if (a.pinned && !b.pinned) return -1;
87-
if (!a.pinned && b.pinned) return 1;
88-
return b.likesCount - a.likesCount;
89-
}),
90-
},
91-
];
92-
93-
const sessionButtons = [
94-
{
95-
key: '공유',
96-
button: (
97-
<div className='flex w-full cursor-pointer flex-row items-center gap-2'>
98-
<IoShareSocialOutline />
99-
<p>공유</p>
100-
</div>
101-
),
102-
onClick: async () => {
103-
const shareUrl = `${window.location.origin}/session/${sessionId}`;
104-
105-
try {
106-
await navigator.clipboard.writeText(shareUrl);
107-
addToast({
108-
type: 'SUCCESS',
109-
message: '세션 링크가 클립보드에 복사되었습니다',
110-
duration: 3000,
111-
});
112-
} catch (err) {
113-
addToast({
114-
type: 'ERROR',
115-
message: '링크 복사에 실패했습니다',
116-
duration: 3000,
117-
});
118-
}
71+
const sections = useMemo(
72+
() => [
73+
{
74+
title: '고정된 질문',
75+
initialOpen: true,
76+
questions: questions
77+
.filter((question) => question.pinned && !question.closed)
78+
.sort((a, b) => b.likesCount - a.likesCount),
11979
},
120-
},
121-
{
122-
key: '호스트 설정',
123-
button: (
124-
<div className='flex w-full cursor-pointer flex-row items-center gap-2'>
125-
<GrValidate />
126-
<p>호스트 설정</p>
127-
</div>
128-
),
129-
onClick: () => openSessionParticipantsModal(),
130-
},
131-
{
132-
key: '세션 종료',
133-
button: (
134-
<div className='flex w-full cursor-pointer flex-row items-center gap-2 text-red-600'>
135-
<IoClose />
136-
<p>세션 종료</p>
137-
</div>
138-
),
139-
onClick: () => openSessionTerminateModal(),
140-
},
141-
];
80+
{
81+
title: '질문',
82+
initialOpen: true,
83+
questions: questions
84+
.filter((question) => !question.pinned && !question.closed)
85+
.sort((a, b) => b.likesCount - a.likesCount),
86+
},
87+
{
88+
title: '답변 완료된 질문',
89+
initialOpen: false,
90+
questions: questions
91+
.filter((question) => question.closed)
92+
.sort((a, b) => {
93+
if (a.pinned && !b.pinned) return -1;
94+
if (!a.pinned && b.pinned) return 1;
95+
return b.likesCount - a.likesCount;
96+
}),
97+
},
98+
],
99+
[questions],
100+
);
101+
102+
const sessionButtons = useMemo(
103+
() => [
104+
{
105+
key: '공유',
106+
button: (
107+
<div className='flex w-full cursor-pointer flex-row items-center gap-2'>
108+
<IoShareSocialOutline />
109+
<p>공유</p>
110+
</div>
111+
),
112+
onClick: async () => {
113+
const shareUrl = `${window.location.origin}/session/${sessionId}`;
114+
115+
try {
116+
await navigator.clipboard.writeText(shareUrl);
117+
addToast({
118+
type: 'SUCCESS',
119+
message: '세션 링크가 클립보드에 복사되었습니다',
120+
duration: 3000,
121+
});
122+
} catch (err) {
123+
addToast({
124+
type: 'ERROR',
125+
message: '링크 복사에 실패했습니다',
126+
duration: 3000,
127+
});
128+
}
129+
},
130+
},
131+
{
132+
key: '호스트 설정',
133+
button: (
134+
<div className='flex w-full cursor-pointer flex-row items-center gap-2'>
135+
<GrValidate />
136+
<p>호스트 설정</p>
137+
</div>
138+
),
139+
onClick: () => openSessionParticipantsModal(),
140+
},
141+
{
142+
key: '세션 종료',
143+
button: (
144+
<div className='flex w-full cursor-pointer flex-row items-center gap-2 text-red-600'>
145+
<IoClose />
146+
<p>세션 종료</p>
147+
</div>
148+
),
149+
onClick: () => openSessionTerminateModal(),
150+
},
151+
],
152+
[sessionId, addToast, openSessionParticipantsModal, openSessionTerminateModal],
153+
);
142154

143155
return (
144156
<>

apps/client/src/features/modal/modal.hook.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode, useContext, useMemo, useState } from 'react';
1+
import { ReactNode, useCallback, useContext, useMemo, useState } from 'react';
22
import { createPortal } from 'react-dom';
33

44
import Background from '@/features/modal/Background';
@@ -8,35 +8,36 @@ export const useModal = (children: ReactNode) => {
88
const [isOpen, setIsOpen] = useState(false);
99
const [isClosing, setIsClosing] = useState(false);
1010

11-
const openModal = () => {
11+
const openModal = useCallback(() => {
1212
setIsOpen(true);
1313
setIsClosing(false);
14-
};
14+
}, []);
1515

16-
const closeModal = () => {
16+
const closeModal = useCallback(() => {
1717
setIsClosing(true);
1818
setTimeout(() => setIsOpen(false), 150);
19-
};
20-
21-
const contextValue = useMemo(() => ({ openModal, closeModal }), []);
19+
}, []);
2220

2321
const Modal = useMemo(() => {
2422
if (!isOpen) return null;
2523
return createPortal(
26-
<ModalContext.Provider value={contextValue}>
24+
<ModalContext.Provider value={{ openModal, closeModal }}>
2725
<Background>
2826
<div className={`modal-content ${isClosing ? 'animate-modalClose' : 'animate-modalOpen'}`}>{children}</div>
2927
</Background>
3028
</ModalContext.Provider>,
3129
document.body,
3230
);
33-
}, [isOpen, isClosing, children, contextValue]);
34-
35-
return {
36-
Modal,
37-
openModal,
38-
closeModal,
39-
};
31+
}, [isOpen, openModal, closeModal, isClosing, children]);
32+
33+
return useMemo(
34+
() => ({
35+
Modal,
36+
openModal,
37+
closeModal,
38+
}),
39+
[Modal, openModal, closeModal],
40+
);
4041
};
4142

4243
export const useModalContext = () => {

apps/client/src/features/socket/socket.context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ interface SocketProviderProps {
1515
}
1616

1717
export function SocketProvider({ children }: SocketProviderProps) {
18-
const { expired, sessionId, sessionToken } = useSessionStore();
18+
const expired = useSessionStore((state) => state.expired);
19+
const sessionId = useSessionStore((state) => state.sessionId);
20+
const sessionToken = useSessionStore((state) => state.sessionToken);
1921

2022
const [socket, setSocket] = useState<SocketService>();
2123

apps/client/src/routes/session/$sessionId/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { loadSessionData, useSessionStore } from '@/features/session';
66
const LazyQuestionList = React.lazy(() => import('@/components').then((module) => ({ default: module.QuestionList })));
77

88
function SessionComponent() {
9-
const { sessionTitle } = useSessionStore();
9+
const sessionTitle = useSessionStore((state) => state.sessionTitle);
1010

1111
useEffect(() => {
1212
if (sessionTitle) {

0 commit comments

Comments
 (0)