설명
- 어떤 react state를 순차적으로 제어하고 싶을때
trigger 용 react state를 생성하여 제어할 수 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
const [target, setTarget] = useState("") const [trigger, setTrigger] = useState("wait") useEffect(() => { setTrigger("first") }, []) useEffect(() => { if (tigger === "first") { setTarget("In first step") setTrigger("second") } }, [target, trigger]) useEffect(() => { if (tigger === "second") { setTarget(target + "| executed second") setTrigger("wait") } }, [target, trigger])
- 문제는 zustand state를 trigger로 사용할때 발생한다.
zustand state를 trigger로 사용하면 두 개의 useEffect가 동시에 실행되기 때문이다. - 이로 인하여 두번째 useEffect에서 참조하는 target은
첫번째 useEffect에서 변경한 target이 아닌 최초 target이다. - 첫 useEffect의 cleanUp 함수에서 setTrigger를 실행시키면
이 문제를 해결할 수 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
const [target, setTarget] = useState("") const [trigger, setTrigger] = useState("wait") useEffect(() => { setTrigger("first") }, []) useEffect(() => { if (tigger === "first") { setTarget("In first step") // #### 변경 시작 #### return () => { setTrigger("second") } // #### 변경 끝 #### } }, [target, trigger]) useEffect(() => { if (tigger === "second") { setTarget(target + "| executed second") setTrigger("wait") } }, [target, trigger])
- cleanUp 함수는 useEffect에서 변경한 사항을 렌더 후 실행되기 때문에
두번째 useEffect에서 첫번째 useEffect가 변경한 target을 참조하게 된다.
예시
- 문제
- 간단한 채팅을 구현하고자 한다.
- 채팅에는 질문, 답변 타입이 있고
타입에 따라 채팅메세지 뒤에 다른 접미어(suffix)가 붙는다. - 최초 질문이 주어져 있고, 사용자가 답변 버튼을 누르면
지정된 답변이 표시된다. - 지정된 답변을 표시할 때에 두 개의 useEffect가 사용된다.
- 첫번째 useEffect는 채팅메세지를 표시하고
두번째 useEffect는 첫번째 useEffect에서
표시한 채팅메세지에 접미어를 붙인다.
- 문제 이미지
- react state를 trigger로 사용하는 경우
- 설명
- step이 trigger이다.
- step을 useState를 이용하여 구현하면
첫 useEffect에서 변경한 chats 값을
다음 useEffect에서 불러올 수 있다.
- 소스코드
- src/app/expt/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
"use client"; import { useEffect, useState } from "react"; type TChatType = "question" | "answer"; interface IChat { chatType: TChatType; content: string; } type TChatSuffix = "|질문" | "|답변"; type TStep = "wait" | "first" | "second"; export default function Page() { const [chats, setChats] = useState<IChat[]>([ { chatType: "question", content: "철수야 뭐해|질문", }, ]); const [step, setStep] = useState<TStep>("wait"); const questionStyle = { backgroundColor: "cyan", border: "1px solid black", padding: "2px", }; const answerStyle = { backgroundColor: "yellow", border: "1px solid black", padding: "2px", }; useEffect(() => { if (step === "first") { const newChat: IChat = { chatType: "answer", content: "응 짱구야 나 밥먹고 있어", }; setChats([...chats, newChat]); setStep("second"); } }, [step, chats]); useEffect(() => { if (step === "second") { console.log("chats IN second", chats); setChats( chats.map((chat, i) => { const suffix = getSuffix(chat.chatType); if (i === chats.length - 1) { chat.content = chat.content + suffix; } return chat; }) ); setStep("wait"); } }, [step, chats]); const getSuffix = (chatType: TChatType): TChatSuffix => { if (chatType === "question") { return "|질문"; } return "|답변"; }; return ( <> <button onClick={() => { setStep("first"); }} > 답변 </button> <div> {chats.map((chat, i) => { if (chat.chatType === "answer") { return ( <div style={answerStyle} key={i}> {chat.content} </div> ); } return ( <div style={questionStyle} key={i}> {chat.content} </div> ); })} </div> </> ); }
- src/app/expt/page.tsx
- 결과
- 설명
- zustand state를 trigger로 사용하고 useEffect cleanUp 함수를 사용하지 않은 경우
- 설명
- step이 trigger이다.
- step을 zustand state를 사용하였다.
- 첫 useEffect에서 chats 값을 변경하였지만
다음 useEffect에서는 여전히 변경 전 chats를 참조하고 있다.
- 소스코드
- store/expt/expt.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { StateCreator } from "zustand"; interface IExptState { step: TStep; setStep: (step: TStep) => void; } type TStep = "wait" | "first" | "second"; export const createExptSlice: StateCreator<IExptState, [], [], IExptState> = ( set ) => ({ step: "wait", setStep: (step: TStep) => set((state) => ({ ...state, step: step })), });
- store/index.tsx
1 2 3 4 5 6 7 8 9
import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { createExptSlice } from "./expt/expt"; export const useBoundStore = create<IExptState>()( devtools((...args) => ({ ...createExptSlice(...args), })) );
- app/expt/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
"use client"; import { useBoundStore } from "@/store"; import { useEffect, useState } from "react"; type TChatType = "question" | "answer"; interface IChat { chatType: TChatType; content: string; } type TChatSuffix = "|질문" | "|답변"; export default function Page() { const [chats, setChats] = useState<IChat[]>([ { chatType: "question", content: "철수야 뭐해|질문", }, ]); // ### 변경 시작 ### const { step, setStep } = useBoundStore((state) => ({ step: state.step, setStep: state.setStep, })); // ### 변경 끝 ### const questionStyle = { backgroundColor: "cyan", border: "1px solid black", padding: "2px", }; const answerStyle = { backgroundColor: "yellow", border: "1px solid black", padding: "2px", }; useEffect(() => { if (step === "first") { console.log("chats IN first", chats); const newChat: IChat = { chatType: "answer", content: "응 짱구야", }; setChats([...chats, newChat]); setStep("second"); } }, [step, chats]); useEffect(() => { if (step === "second") { console.log("chats IN second", chats); setChats( chats.map((chat, i) => { const suffix = getSuffix(chat.chatType); if (i === chats.length - 1) { chat.content = chat.content + suffix; } return chat; }) ); setStep("wait"); } }, [step, chats]); const getSuffix = (chatType: TChatType): TChatSuffix => { if (chatType === "question") { return "|질문"; } return "|답변"; }; return ( <> <button onClick={() => { setStep("first"); }} > 답변 </button> <div> {chats.map((chat, i) => { if (chat.chatType === "answer") { return ( <div style={answerStyle} key={i}> {chat.content} </div> ); } return ( <div style={questionStyle} key={i}> {chat.content} </div> ); })} </div> </> ); }
- store/expt/expt.tsx
- 결과
- 설명
- zustand state를 trigger로 사용하고 useEffect cleanUp 함수 사용한 경우
- 설명
- step이 trigger이다.
- step을 zustand state를 사용하였다.
- 첫 useEffect 실행 후 cleanUp 함수에서 step을 second으로 변경한다.
- 첫 useEffect에서 변경한 chats 값을
다음 useEffect에서 불러올 수 있다.
- 소스코드
- store/expt/expt.tsx
- 위 예시와 동일
- store/index.tsx
- 위 예시와 동일
- app/expt/page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
"use client"; import { useBoundStore } from "@/store"; import { useEffect, useState } from "react"; type TChatType = "question" | "answer"; interface IChat { chatType: TChatType; content: string; } type TChatSuffix = "|질문" | "|답변"; export default function Page() { const [chats, setChats] = useState<IChat[]>([ { chatType: "question", content: "철수야 뭐해|질문", }, ]); const { step, setStep } = useBoundStore((state) => ({ step: state.step, setStep: state.setStep, })); const questionStyle = { backgroundColor: "cyan", border: "1px solid black", padding: "2px", }; const answerStyle = { backgroundColor: "yellow", border: "1px solid black", padding: "2px", }; useEffect(() => { if (step === "first") { console.log("chats IN first", chats); const newChat: IChat = { chatType: "answer", content: "응 짱구야", }; setChats([...chats, newChat]); // ### 변경 시작 ### return () => { setStep("second"); }; // ### 변경 끝 ### } }, [step, chats]); useEffect(() => { if (step === "second") { console.log("chats IN second", chats); setChats( chats.map((chat, i) => { const suffix = getSuffix(chat.chatType); if (i === chats.length - 1) { chat.content = chat.content + suffix; } return chat; }) ); setStep("wait"); } }, [step, chats]); const getSuffix = (chatType: TChatType): TChatSuffix => { if (chatType === "question") { return "|질문"; } return "|답변"; }; return ( <> <button onClick={() => { setStep("first"); }} > 답변 </button> <div> {chats.map((chat, i) => { if (chat.chatType === "answer") { return ( <div style={answerStyle} key={i}> {chat.content} </div> ); } return ( <div style={questionStyle} key={i}> {chat.content} </div> ); })} </div> </> ); }
- store/expt/expt.tsx
- 결과
- 설명
덧붙이는 말
- 첫 useEffect 실행결과를 렌더시킨 후
다음 useEffect를 실행시키면 해결되기 때문에
useEffect cleanUp 대신 setTimeout을 사용하여 문제를 해결할 수도 있다.