'TaskTracker'

πŸ… TaskTracker - 회고

🎯 ν”„λ‘œμ νŠΈ κ°œμš” λ³Έ ν”„λ‘œμ νŠΈκ°€ μ§€ν–₯ν•˜λŠ” λ°”λŠ” λ‹€μŒκ³Ό κ°™μ•˜λ‹€. ν”„λ‘œμ νŠΈμ˜ ν•„μš”μ„±μ„ 본인 슀슀둜 납득할 수 μžˆμ„ 것 Firebaseλ₯Ό ν†΅ν•œ Authenticationκ³Ό CRUD의 μ „ 과정을 κ²½ν—˜ν•  것 Redux-Toolkit을 톡해 μ „μ—­ μƒνƒœ 관리λ₯Ό κ²½ν—˜ν•  것 >

2024λ…„ 6μ›” 22일10min read

https://tasktracker-livid.vercel.app/home

🎯 ν”„λ‘œμ νŠΈ κ°œμš”

λ³Έ ν”„λ‘œμ νŠΈκ°€ μ§€ν–₯ν•˜λŠ” λ°”λŠ” λ‹€μŒκ³Ό κ°™μ•˜λ‹€.

1. ν”„λ‘œμ νŠΈμ˜ ν•„μš”μ„±μ„ 본인 슀슀둜 납득할 수 μžˆμ„ 것 2. Firebaseλ₯Ό ν†΅ν•œ Authenticationκ³Ό CRUD의 μ „ 과정을 κ²½ν—˜ν•  것 3. Redux-Toolkit을 톡해 μ „μ—­ μƒνƒœ 관리λ₯Ό κ²½ν—˜ν•  것

βœ… ν”„λ‘œμ νŠΈμ˜ ν•„μš”μ„±

λΆˆνŽΈν–ˆλ‹€. κ³„νšμ„ μ„Έμš°λ©΄ μ§€ν‚€μ§€ μ•Šκ³ , μ„Έμš°μ§€ μ•ŠμœΌλ©΄ λΆˆμ•ˆν–ˆλ‹€. 그리고 λͺ¨λ“  것을 μ˜ˆμΈ‘ν•  만큼 λ˜‘λ˜‘ν•˜μ§€ μ•Šμ•˜λ‹€. 맀번 μˆ˜ν–‰ν•˜λ €λŠ” μž‘μ—…μ˜ λ‚œμ΄λ„λŠ” μƒμ΄ν–ˆκ³ , μ˜ˆμΈ‘ν•  수 μ—†λŠ” 무언가λ₯Ό μ˜ˆμΈ‘ν•˜λ € ν–ˆκΈ°μ— λ‹Ήμ—°νžˆ μ‹€νŒ¨ν•˜λ©΄μ„œ, 무엇인가 잘λͺ»λλ‹€λŠ” 생각이 λ“€μ—ˆλ‹€.

원인과 κ²°κ³Όκ°€ λ°”λ€ŒλŠ” μˆœκ°„μ΄ λΆ„λͺ…νžˆ μžˆλ‹€. κ·ΈλŸ¬λ‹ˆκΉŒ, κ³„νšκ³Ό 싀행에 λ¬Έμ œκ°€ μžˆμ–΄μ„œ λΆˆμ•ˆν•΄μ§€λŠ” 것이 μ•„λ‹ˆλΌ, λΆˆμ•ˆν•˜κΈ° λ•Œλ¬Έμ— κ³„νšκ³Ό 싀행이 λ§κ°€μ§€λŠ” 상황이 μžˆλ‹€λŠ” 것이닀. λΆˆμ•ˆμ΄ 원인이라면 그것을 κ²½κ°ν•˜λ €λŠ” μͺ½μ΄ 해결에 가깝겠닀.

λ½€λͺ¨λ„λ‘œ 기법은 κ½€λ‚˜ 유λͺ…ν•œ μ‹œκ°„κ΄€λ¦¬ 방법둠 쀑 ν•˜λ‚˜μ˜€κ³ , λ²„νŠΌμ„ ν΄λ¦­ν•˜λŠ” μˆœκ°„ λ‚΄κ°€ μ„€μ •ν•œ Task와 μ‹œκ°„μ— μ˜¨μ „νžˆ μ§‘μ€‘ν•˜κ²Œ λœλ‹€λŠ” 점이 λΆˆμ•ˆ 관리에 μƒλ‹Ήνžˆ μœ μš©ν•˜λ‹€λŠ” 생각이 λ“€μ—ˆλ‹€.

ν”„λ‘œμ νŠΈλŠ” 사싀상 껍데기에 λΆˆκ³Όν•˜λ‹€. ν”„λ‘œμ νŠΈλΌλŠ” λ°”μš΄λ”λ¦¬ μ•ˆμ—μ„œ μ–΄λ– ν•œ κΈ°μˆ μ„ μŠ΅λ“ν•˜κ³  μˆ™λ ¨ν–ˆμœΌλ©° 무슨 이야기λ₯Ό λ‹΄μ•˜λŠ”μ§€κ°€ 훨씬 μ€‘μš”ν•˜κ³ , μ΄λŸ¬ν•œ μž‘μ—…μ€ μ—¬μ „νžˆ μ–΄λ ΅μ§€λ§Œ, 고민의 μ΄λŸ‰μ΄ ν”„λ‘œμ νŠΈμ΄κΈ° λ•Œλ¬Έμ— 고민이 μ „μ œλ˜μ§€ μ•Šμ€ ν”„λ‘œμ νŠΈλ₯Ό μˆ˜ν–‰ν•  것이라면, κ·Έλƒ₯ λ‚˜κ°€μ„œ λ…ΈλŠ” 게 행볡에 더 μœ λ¦¬ν•  κ²ƒμ΄λΌλŠ” 것이 λ‚΄ μƒκ°μ΄μ—ˆλ‹€. 역섀적이닀.

βœ… Authentication

1. createUserWithEmailAndPassword(νšŒμ› κ°€μž…) ✍️

code
  // νšŒμ›κ°€μž…μ„ μ²˜λ¦¬ν•˜λŠ” ν•¨μˆ˜
  const handleSignUp = async (e) => {
    e.preventDefault();
    if (Object.values(errors).some((error) => error)) {
      alert("Please check your information again.");
      return;
    }
    await dispatch(
      signupUser({
        email: userInfo?.email,
        password: userInfo?.password,
        imgFile,
        displayName: userInfo?.username,
      })
    ).unwrap();

    alert("Sign up successful!");
    navigate("/signin");
  };
code
export const signupUser = createAsyncThunk(
  "user/signupUser",
  async (userInfo) => {
    try {
      // Firebaseλ₯Ό 톡해, 이메일과 λΉ„λ°€λ²ˆν˜Έλ‘œ μ‚¬μš©μžλ₯Ό 생성
      await createUserWithEmailAndPassword(
        auth,
        userInfo?.email,
        userInfo?.password
      );

      // ν˜„μž¬ μ‚¬μš©μžκ°€ μžˆλŠ” 경우(=firebase에 νšŒμ› 등둝이 된 경우)
      if (auth.currentUser) {
        // λ™μ‹œμ— 이미지 파일이 μžˆλŠ” 경우
        if (userInfo?.imgFile) {
          // firebase에 이미지λ₯Ό μ—…λ‘œλ“œ
          const storageRef = ref(
            storage,
            new Date().getTime() + userInfo?.imgFile?.name
          );
          const uploadTask = uploadBytesResumable(
            storageRef,
            userInfo?.imgFile
          );
          uploadTask.on(
            "state_changed",
            // μ—…λ‘œλ“œ μƒνƒœκ°€ 변경될 λ•Œ 싀행될 콜백 ν•¨μˆ˜
            (snapshot) => {
              // μ—…λ‘œλ“œ μ§„ν–‰λ₯ μ„ κ³„μ‚°ν•˜λŠ” 둜직
              const progress =
                (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
              console.log("Upload is " + progress + "% done");
              switch (snapshot.state) {
                case "paused":
                  console.log("Upload is paused");
                  break;
                case "running":
                  console.log("Upload is running");
                  break;
              }
            },
            // μ—…λ‘œλ“œ 쀑 μ—λŸ¬κ°€ λ°œμƒν•œ 경우
            (error) => {
              alert("Failed to upload image. Please try again later.");
            },
            // μ—…λ‘œλ“œκ°€ μ™„λ£Œλœ 경우
            () => {
              // μ—…λ‘œλ“œλœ 파일의 λ‹€μš΄λ‘œλ“œ URL을 κ°€μ Έμ˜΄
              getDownloadURL(uploadTask.snapshot.ref).then(
                async (downloadURL) => {
                  // μ‚¬μš©μž ν”„λ‘œν•„μ„ μ—…λ°μ΄νŠΈ
                  await updateProfile(auth.currentUser, {
                    displayName: userInfo?.displayName,
                    photoURL: downloadURL,
                  });
                }
              );
            }
          );
        }
        // 이미지 파일이 μ—†λŠ” 경우
        else {
          await updateProfile(auth.currentUser, {
            displayName: userInfo?.displayName,
            photoURL: "",
          });
        }
      }
    } catch (error) {
      // μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우
      switch (error.code) {
        case "auth/email-already-in-use":
          alert("Email already in use.");
          break;
        case "auth/weak-password":
          alert("Password should be at least 6 characters.");
          break;
        case "auth/network-request-failed":
          alert("Network request failed.");
          break;
        case "auth/invalid-email":
          alert("Invalid email format.");
          break;
        case "auth/internal-error":
          alert("Internal error.");
          break;
        default:
          alert("Sign up failed.");
      }
    }
  }
);

μ½”λ“œλ₯Ό 주절주절 μ„€λͺ…ν•˜λŠ” 것은 크게 μ˜λ―Έκ°€ 없을 것 κ°™λ‹€. Firebaseμ—μ„œ μ œκ³΅ν•˜λŠ” createUserWithEmailAndPassword ν•¨μˆ˜μ— μ μ ˆν•œ 값을 μ „λ‹¬ν•˜λ©΄ νšŒμ› κ°€μž…μ€ μ™„λ£Œλœλ‹€.

그런데 μž¬λ£Œκ°€ 무엇인지 κ·Έ 속성이 μ–΄λ– ν•œμ§€λŠ” λͺ…ν™•νžˆ 인지해야 ν•œλ‹€. ν–₯후에 React-Queryλ‚˜ Supabase λ“± λ‹€μ–‘ν•œ κΈ°μˆ μ„ ν”„λ‘œμ νŠΈμ— μ μš©ν•  μ˜ˆμ •μΈλ°, λ‚΄κ°€ ν™œμš©ν•  수 μžˆλŠ” λ°μ΄ν„°μ—λŠ” 무엇이 있으며, κ·Έ 데이터듀이 κ°€μ§€λŠ” 속성이 무엇인지 μ•Œμ§€ λͺ»ν•˜λ©΄, μ œλŒ€λ‘œ 된 μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μ—†λ‹€.

돼지고기가 μ†Œν™”λ˜λ©΄ νŽ©νƒ€μ΄λ“œλ₯Ό 거쳐 μ•„λ―Έλ…Έμ‚°μœΌλ‘œ λ°”λ€ŒλŠ”λ°, μ΄λ•Œ ν•„μš”ν•œ 것이 λ‹¨λ°±μ§ˆ λΆ„ν•΄ νš¨μ†ŒμΈ 'ν”„λ‘œν…Œμ•„μ œ'κ³ , μƒˆμš°μ “μ΄ λ°œνš¨λ˜λŠ” λ™μ•ˆ μƒμ„±λ˜λŠ” ν”„λ‘œν…Œμ•„μ œλŠ” μΌμ’…μ˜ μ†Œν™”μ œ 역할을 ν•˜κΈ°μ—, 일반적으둜 μˆœλŒ“κ΅­μ˜ 간을 μƒˆμš°μ “μœΌλ‘œ ν•œλ‹€.

코딩은 지식이 μ••λ„μ μœΌλ‘œ λ§Žμ€, κΉŒλ‹€λ‘œμš΄ μ†λ‹˜(like 백쒅원 μ„ μƒλ‹˜)μ—κ²Œ 식사λ₯Ό λŒ€μ ‘ν•˜λŠ” κ³Όμ •κ³Ό κ°™μ•„μ„œ, λ‚΄κ°€ μ‚¬μš©ν•˜λŠ” 재료λ₯Ό λͺ…ν™•νžˆ μ•Œκ³  μžˆμ–΄μ•Όλ§Œ ν•œλ‹€.

2. signInWithEmailAndPassword(둜그인) ✍️

code
 // λ‘œκ·ΈμΈμ„ μ²˜λ¦¬ν•˜λŠ” ν•¨μˆ˜
  const handleSignIn = async () => {
    try {
      await dispatch(
        signInUser({ email: signinInfo?.email, password: signinInfo?.password })
      ).unwrap();

      navigate("/home");
    } catch (error) {
      console.error(error);
    }
  };
code
export const signInUser = createAsyncThunk(
  "user/signInUser",
  async (userInfo) => {
    const userCredential = await signInWithEmailAndPassword(
      auth,
      userInfo?.email,
      userInfo?.password
    );
    const result = {
      uid: userCredential?.user?.uid,
      displayName: userCredential?.user?.displayName,
      email: userCredential?.user?.email,
    };
    localStorage.setItem("user", JSON.stringify(result));

    return result;
  }
);

Firebaseμ—μ„œ μ œκ³΅ν•˜λŠ” ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•œλ‹€λŠ” 점을 μ œμ™Έν•˜κ³  νŠΉμ§•μ μΈ 것은, μœ μ € 정보λ₯Ό localstorage에 μ €μž₯ν•œλ‹€λŠ” 점이닀.

μ—¬λŸ¬ μž₯단점이 μžˆκ² μ§€λ§Œ, λΈŒλΌμš°μ €μ— 데이터λ₯Ό μ €μž₯ν•˜λ©΄, μœ μ €κ°€ νŽ˜μ΄μ§€λ₯Ό μƒˆλ‘œκ³ μΉ¨ν•˜κ±°λ‚˜ λΈŒλΌμš°μ €λ₯Ό λ‹«μ•˜λ‹€κ°€ λ‹€μ‹œ 열어도, localstorage에 μ €μž₯된 λ°μ΄ν„°λŠ” κ·ΈλŒ€λ‘œ μœ μ§€λ˜κΈ°μ—, 둜그인 μƒνƒœ μœ μ§€ λ“± μ„Έμ…˜ κ΄€λ ¨ κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λŠ” 데 μœ μš©ν•˜κΈ° λ•Œλ¬Έμ— μ‚¬μš©ν–ˆλ‹€.

3. onAuthStateChanged(νšŒμ› μœ μ§€) ✍️

code
  useEffect(() => {
    dispatch(checkLoginUser(JSON.parse(localStorage.getItem("user"))));
  }, [dispatch]);
code
// μœ μ € 정보λ₯Ό μ„€μ •ν•˜λŠ” 비동기 ν•¨μˆ˜
export const checkLoginUser = createAsyncThunk(
  "user/checkLoginUser",
  async (user) => {
    console.log(user);
    const auth = getAuth();
    onAuthStateChanged(auth, (user) => {
      console.log(user);
      localStorage.setItem("user", JSON.stringify(user));
    });

    return user;
  }
);

Appbarμ—μ„œλŠ” ν•΄λ‹Ή useEffectλ₯Ό μ‹€ν–‰ν•œλ‹€. 즉, Appbarκ°€ μ œμ‹œλ˜λŠ” λͺ¨λ“  μ˜μ—­μ—μ„œ κ³„μ†ν•΄μ„œ μœ μ € 정보λ₯Ό checkLoginUser ν•¨μˆ˜λ‘œ dispatch ν•œλ‹€λŠ” 것이닀.

Firebaseμ—μ„œ μ œκ³΅ν•˜λŠ” onAuthStateChanged ν•¨μˆ˜λ₯Ό 톡해 κ³„μ†ν•΄μ„œ μœ μ € 정보λ₯Ό κ°±μ‹  및 μœ μ§€ν•  수 있게 λœλ‹€.

βœ… CRUD

1. Add Task λ²„νŠΌ ✍️

λ™μž‘: μ‚¬μš©μžκ°€ μž…λ ₯ν•œ μƒˆλ‘œμš΄ νƒœμŠ€ν¬ 이름을 λ¦¬μŠ€νŠΈμ— μΆ”κ°€ν•œλ‹€. 예λ₯Ό λ“€μ–΄, μ‚¬μš©μžκ°€ "μš΄λ™ν•˜κΈ°"λΌλŠ” νƒœμŠ€ν¬λ₯Ό μž…λ ₯ν•˜λ©΄, 이 νƒœμŠ€ν¬κ°€ λ¦¬μŠ€νŠΈμ— μΆ”κ°€λœλ‹€.

2. Go to Archive λ²„νŠΌ ✍️

λ™μž‘: ν˜„μž¬ ν™”λ©΄μ—μ„œ μ•„μΉ΄μ΄λΈŒ νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€. 이동 ν›„μ—λŠ” ν™ˆ νŽ˜μ΄μ§€μ— 있던 λͺ¨λ“  complete νƒœμŠ€ν¬λ“€μ„ 확인할 수 μžˆλ‹€.

3. All Clear λ²„νŠΌ ✍️

λ™μž‘: μ•„μΉ΄μ΄λΈŒ νŽ˜μ΄μ§€μ— μžˆλŠ” λͺ¨λ“  complete νƒœμŠ€ν¬λ“€μ„ μ‚­μ œν•œλ‹€. 단, ν™ˆ νŽ˜μ΄μ§€μ— μΆ”κ°€λœ λ‚΄μš©μ€ completeκ°€ μ•„λ‹ˆκΈ°μ— μ‚­μ œλ˜μ§€ μ•ŠλŠ”λ‹€.

4. Go Home λ²„νŠΌ ✍️

λ™μž‘: μ•„μΉ΄μ΄λΈŒ νŽ˜μ΄μ§€μ—μ„œ ν™ˆ νŽ˜μ΄μ§€λ‘œ λ˜λŒμ•„κ°„λ‹€. 이동 ν›„μ—λŠ” λ‹€μ‹œ ν™ˆ νŽ˜μ΄μ§€μ— μΆ”κ°€λ˜μ–΄ 있던 νƒœμŠ€ν¬λ“€κ³Ό μ•„μΉ΄μ΄λΈŒ λ²„νŠΌμ„ 확인할 수 μžˆλ‹€.

5. Start λ²„νŠΌ ✍️

λ™μž‘: μ„ νƒν•œ νŠΉμ • νƒœμŠ€ν¬μ˜ 타이머λ₯Ό μ‹œμž‘ν•œλ‹€. 예λ₯Ό λ“€μ–΄, "μš΄λ™ν•˜κΈ°" νƒœμŠ€ν¬μ˜ 타이머λ₯Ό μ‹œμž‘ν•˜λ©΄, ν•΄λ‹Ή νƒœμŠ€ν¬λŠ” 'pending' μƒνƒœλ‘œ ν‘œμ‹œλ˜λ©° μ‹œκ°„μ΄ μ§„ν–‰λœλ‹€.

6. Pause λ²„νŠΌ ✍️

λ™μž‘: ν˜„μž¬ μ§„ν–‰ 쀑인 νƒœμŠ€ν¬μ˜ 타이머λ₯Ό μΌμ‹œ μ •μ§€ν•œλ‹€. 예λ₯Ό λ“€μ–΄, "μš΄λ™ν•˜κΈ°" νƒœμŠ€ν¬μ˜ 타이머가 μΌμ‹œ μ •μ§€λ˜λ©΄, ν•΄λ‹Ή νƒœμŠ€ν¬λŠ” 'paused' μƒνƒœλ‘œ λ³€κ²½λ˜λ©° μ •μ§€λœ 기둝으둜 ν•΄λ‹Ή νƒœμŠ€ν¬κ°€ μ„œλ²„μ— μ €μž₯λœλ‹€.

7. Delete λ²„νŠΌ ✍️

λ™μž‘: μ„ νƒν•œ νŠΉμ • νƒœμŠ€ν¬λ₯Ό λ¦¬μŠ€νŠΈμ—μ„œ μ‚­μ œν•œλ‹€. 예λ₯Ό λ“€μ–΄, "μš΄λ™ν•˜κΈ°" νƒœμŠ€ν¬λ₯Ό μ‚­μ œν•˜λ©΄, 이 νƒœμŠ€ν¬λŠ” λ¦¬μŠ€νŠΈμ—μ„œ μ œκ±°λ˜μ–΄ UIμ—μ„œ 보이지 μ•Šκ²Œ λœλ‹€.

8. λŒ€ν‘œ μ½”λ“œ ✍️

code
  const handleAddTask = () => {
    if (!task.trim()) {
      return alert("νƒœμŠ€ν¬λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.");
    }
    const newTask = {
      createdAt: Date.now(),
      task,
      status: "pending",
      initialTime: time,
      pausedTime: "",
      userId: user.uid,
    };
    dispatch(addTask(newTask));
    setTask("");
    setTime(300);
  };
code
import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
  addDoc,
  getDocs,
  deleteDoc,
  doc,
  collection,
  updateDoc,
} from "firebase/firestore";
import { db } from "../firebase";

// λͺ¨λ“  νƒœμŠ€ν¬ κ°€μ Έμ˜€κΈ°
export const getAllTasks = createAsyncThunk(
  "tasks/getAllTasks",
  async (userId) => {
    const taskCollection = await getDocs(collection(db, "tasks"));
    const allTasks = [
      ...taskCollection.docs.map((doc) => ({ ...doc.data(), id: doc.id })),
    ].filter((task) => task.userId === userId);
    return allTasks;
  }
);

// νƒœμŠ€ν¬ μΆ”κ°€ν•˜κΈ°
export const addTask = createAsyncThunk("tasks/addTask", async (data) => {
  const result = await addDoc(collection(db, "tasks"), {
    userId: data?.userId,
    task: data?.task,
    initialTime: data?.initialTime,
    pausedTime: "",
    createdAt: data?.createdAt,
    status: data.status,
  });
  const taskId = result.path.split("/")[1];
  return { ...data, id: taskId };
});

// νƒœμŠ€ν¬ μ‚­μ œν•˜κΈ°
export const deleteTask = createAsyncThunk(
  "tasks/deleteTask",
  async (taskId) => {
    console.log(taskId);
    await deleteDoc(doc(db, "tasks", taskId));

    return taskId;
  }
);

// 멈좘 νƒœμŠ€ν¬ μΆ”κ°€ν•˜κΈ°
export const putTask = createAsyncThunk("tasks/putTask", async (task) => {
  if (task.pausedTime !== "") {
    await updateDoc(doc(db, "tasks", task.id), {
      pausedTime: task?.pausedTime,
      status: task.status,
    });
  }
  return task;
});

// μ™„λ£Œλœ νƒœμŠ€ν¬ μΆ”κ°€ν•˜κΈ°
export const completedTask = createAsyncThunk(
  "tasks/completedTask",
  async (task) => {
    if (task?.status === "complete") {
      await updateDoc(doc(db, "tasks", task.id), {
        status: task?.status,
      });
    }

    return task;
  }
);

// λͺ¨λ“  'complete task' μ‚­μ œν•˜κΈ°
export const allClear = createAsyncThunk(
  "tasks/allClearTask",
  async (userId) => {
    const taskCollection = await getDocs(collection(db, "tasks"));
    const tasksToDelete = taskCollection.docs.filter(
      (doc) => doc.data().userId === userId && doc.data().status === "complete"
    );

    const deletePromises = tasksToDelete.map((task) =>
      deleteDoc(doc(db, "tasks", task.id))
    );
    await Promise.all(deletePromises);

    return tasksToDelete.map((task) => task.id);
  }
);

const initialState = {
  tasks: [],
  loading: false,
  error: null,
};

const tasksSlice = createSlice({
  name: "tasks",
  initialState,
  extraReducers: (builder) => {
    builder.addCase(getAllTasks.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(getAllTasks.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks = action.payload;
    });
    builder.addCase(getAllTasks.rejected, (state) => {
      state.loading = false;
    });
    builder.addCase(addTask.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(addTask.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks.push(action.payload);
    });
    builder.addCase(addTask.rejected, (state) => {
      state.loading = false;
    });
    builder.addCase(deleteTask.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(deleteTask.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks = state.tasks.filter((task) => task.id !== action.payload);
    });
    builder.addCase(deleteTask.rejected, (state) => {
      state.loading = false;
    });
    builder.addCase(putTask.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(putTask.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks = state.tasks.map((task) =>
        task.id === action.payload.id ? action.payload : task
      );
    });
    builder.addCase(putTask.rejected, (state) => {
      state.loading = false;
    });
    builder.addCase(completedTask.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(completedTask.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks = state.tasks.filter((task) => task.id !== action.payload.id);
      window.alert("Task saved successfully πŸ˜„");
    });
    builder.addCase(completedTask.rejected, (state) => {
      state.loading = false;
    });
    builder.addCase(allClear.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(allClear.fulfilled, (state, action) => {
      state.loading = false;
      state.tasks = state.tasks.filter((task) => !action.payload.some(task.id));
    });
    builder.addCase(allClear.rejected, (state) => {
      state.loading = false;
    });
  },
});

export default tasksSlice.reducer;

λ‚˜μ—κ²Œ ν•„μš”ν•œ λ°μ΄ν„°μ˜ ν”„λ‘œνΌν‹°λ₯Ό λͺ…ν™•νžˆ ν•¨μœΌλ‘œμ¨, db의 뢄산을 쀄여, 효율적인 μƒνƒœ 관리λ₯Ό μˆ˜ν–‰ν•˜κ² λ‹€λŠ” 것이 핡심이닀.

βœ… μ˜ˆμ™Έμ²˜λ¦¬

1. νšŒμ› κ°€μž…μ— ν•„μš”ν•œ μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό util둜 뢄리 ✍️

code
export const validateUsername = (username) => {
  const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/;
  return usernameRegex.test(username)
    ? ""
    : "3자 이상 16자 μ΄ν•˜, 영문자, 숫자, 밑쀄(_)만 ν—ˆμš©λ©λ‹ˆλ‹€.";
};

export const validateEmail = (email) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email) ? "" : "잘λͺ»λœ 이메일 ν˜•μ‹μž…λ‹ˆλ‹€.";
};

export const validatePassword = (password) => {
  const passwordRegex =
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return passwordRegex.test(password)
    ? ""
    : "μ΅œμ†Œ 8자 이상, λŒ€λ¬Έμž, μ†Œλ¬Έμž, 숫자, 특수문자λ₯Ό 포함해야 ν•©λ‹ˆλ‹€.";
};

2. 둜그인 여뢀에 λ”°λ₯Έ λΌμš°νŒ… μ˜ˆμ™Έ 처리 ✍️

code
import { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";

const usePreventAuth = () => {
  const navigate = useNavigate();
  const { pathname } = useLocation();

  useEffect(() => {
    if (JSON.parse(localStorage.getItem("user"))) {
      navigate("/home");
      if (pathname === "/archive") {
        navigate("/archive");
      }
    } else {
      if (pathname === "/home" || pathname === "/archive") {
        navigate("/");
      }
    }
  }, []);
  return;
};

export default usePreventAuth;

3. loading을 ν†΅ν•œ, (API 톡신 μ „) Button disabled 처리 ✍️

code
 <Button disabled={loading} onClick={handleSignIn}>
            Sign In
          </Button>
code
const Button = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  width: 300px;
  padding: 1rem 0;
  margin: 1rem;
  font-size: 1.5rem;
  color: black;
  background: white;
  border: 2px solid white;
  border-radius: 50px;
  box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
  transition: all 0.3s ease-in-out;
  cursor: pointer;
  opacity: 0;
  animation: fadeIn 1.5s ease forwards;

  @keyframes fadeIn {
    from {
      opacity: 0;
      transform: translateY(20px);
    }
    to {
      opacity: 1;
    }
  }

  &:hover {
    background: #ffffff;
    color: black;
    transform: translateY(-5px);
    box-shadow: 0px 8px 20px rgba(255, 255, 255, 0.3);
  }

  &:active {
    transform: translateY(0);
    box-shadow: 0px 4px 15px rgba(255, 255, 255, 0.2);
  }
  &:disabled {
    background-color: gray;
  }
`;

4. .envλ₯Ό ν†΅ν•œ ν™˜κ²½λ³€μˆ˜ 처리 ✍️

code
import { initializeApp } from "firebase/app";
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: "task-tracker3.firebaseapp.com",
  projectId: "task-tracker3",
  storageBucket: "task-tracker3.appspot.com",
  messagingSenderId: "977756658453",
  appId: "1:977756658453:web:588402c61ac23db27c671c",
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const storage = getStorage(app); // Initialize storage

export { auth, db, storage, onAuthStateChanged };

βœ… 회고

슀무 살이 κ°“ λ„˜μ—ˆμ„ λ•Œ 잠깐 νŒŒλ¦¬λ°”κ²Œλœ¨μ—μ„œ μ•„λ₯΄λ°”μ΄νŠΈλ₯Ό ν•œ 적이 μžˆλ‹€. ν•œ ν• λ¨Έλ‹ˆκ°€ λ”°λ“―ν•œ 물을 쀄 수 μžˆλƒκ³  μ—¬μ­€λ³΄μ…”μ„œ 뜨거운 물에 찬물을 μ’€ 타고 μžˆμ—ˆλŠ”λ°, μ‹€μž₯λ‹˜μ΄ 찬물을 μ™œ 뢓냐고 ν˜Έν†΅ μ•„λ‹Œ ν˜Έν†΅μ„ μ³€λ‹€. 그런데 ν• λ¨Έλ‹˜μ€ μ›ƒμœΌμ…¨λ‹€. '뜨거운' 물이 μ•„λ‹ˆλΌ, 'λ”°λ“―ν•œ' 물을 달라고 ν–ˆμœΌλ‹ˆ λ‚΄κ°€ λ§žλ‹€κ³  λ§μ”€ν•˜μ…¨λ‹€. λ¬Όλ‘  μ‹€μž₯λ‹˜λ„ μ‚¬κ³Όν•˜μ…¨λ‹€.

감동은 λ””ν…ŒμΌμ—μ„œ μ˜¨λ‹€. μž¬λ°ŒλŠ” 점은, κ·Έ λ””ν…ŒμΌμ΄λΌλŠ” 것은 λŒ€λ‹¨ν•œ νŒλ‹¨μ΄ μ•„λ‹ˆλΌ μ„Έμ‹¬ν•œ κ΄€μ°°λ‘œλΆ€ν„° 얻을 수 μžˆλ‹€. 머리털 빠질 λ•ŒκΉŒμ§€ 쳐닀본 ν”„λ‘œλ•νŠΈλŠ” 퀄리티가 쒋을 μˆ˜λ°–μ— μ—†κ³ , μ•„λ§ˆλ„ μ‚¬μš©μžμ—κ²Œ κ°λ™μœΌλ‘œ λ‹€κ°€κ°ˆ 것이닀. λ¬Όλ‘  κ·Έλ ‡μ§€ μ•Šλ”λΌλ„ λ‹Ήμ—°νžˆ 많이 κ΄€μ°°ν•΄μ•Ό λœλ‹€. κΈ°λ³Έμ΄λ‹ˆκΉŒ. 'λΆ€μ§€λŸ°ν•¨'에 λŒ€ν•œ 경각심이 이번 ν”„λ‘œμ νŠΈμ˜ μ΅œλŒ€ κ΅ν›ˆ μ•„λ‹ˆμ—ˆμ„κΉŒ.

More to read

Amazon VPC

Amazon VPC Architecture μ΄ν•΄ν•˜κΈ°

μƒˆλ‘œμš΄ ν”„λ‘œμ νŠΈλ₯Ό κΈ°νšν•˜λ©°, κ°œλ°œμ—μ„œ 무엇을 κ°€μž₯ λ¨Όμ € κ³ λ―Όν•΄μ•Ό ν•˜λŠ”μ§€ λ‹€μ‹œ λŒμ•„λ³΄κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.ν•œλ•ŒλŠ” ν”„λ‘ νŠΈμ—”λ“œκ°€ λͺ¨λ“  μ„€κ³„μ˜ 좜발점이라고 λ―Ώμ—ˆμŠ΅λ‹ˆλ‹€. μœ μ €κ°€ 무엇을 보고, μ–΄λ–€ νλ¦„μ—μ„œ 머무λ₯΄κ³  μ΄νƒˆν•˜λŠ”μ§€μ— λŒ€ν•œ 이해 없이 μ„œλΉ„μŠ€λ₯Ό λ§Œλ“ λ‹€λŠ” 건 λΆˆκ°€λŠ₯ν•˜λ‹€κ³  μƒκ°ν–ˆκΈ°

'μ›μ‚¬μ΄νŠΈ'

ν”„λ‘ νŠΈμ—”λ“œ κ΄€μ μœΌλ‘œ μ•Œκ³ λ¦¬μ¦˜ μ΄ν•΄ν•˜κΈ°

μ˜€λžœλ§Œμ— 방법둠에 κ΄€ν•œ 글을 μ“°κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 졜근 상황은 μ΄λ ‡μŠ΅λ‹ˆλ‹€. SSAFYμ—μ„œλŠ” ν•˜λ£¨μ— μ—„μ²­λ‚œ μ–‘μ˜ μ•Œκ³ λ¦¬μ¦˜ λ¬Έμ œλ“€μ„ 과제둜 μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. κ·Έ κ³Όμ •μ—μ„œ, 'κ΅¬ν˜„λ ₯'이 맀우 λ–¨μ–΄μ§„λ‹€λŠ” 생각이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. μ™„μ „νžˆ μ–΄λ €μš΄ 문제라면 '아쉬움'μ΄λΌλŠ” 감정쑰차 λŠλΌμ§€

Subnet

VPC μ„€κ³„μ˜ μ‹œμž‘: IP와 Subnet

λ°˜λ³΅λ˜λŠ” 루틴 μ†μ—μ„œ 얻은 μ•ˆμ •κ°μ„ 발판 μ‚Όμ•„, μ΄μ œλŠ” 기술적 μŠ€νŽ™νŠΈλŸΌμ„ λ„“νžˆκΈ° μœ„ν•œ 개인 ν”„λ‘œμ νŠΈμ— μ°©μˆ˜ν•˜κ³ μž ν•©λ‹ˆλ‹€.이번 ν”„λ‘œμ νŠΈμ˜ λͺ©ν‘œλŠ” λ‹¨μˆœν•œ 포트폴리였 ꡬ좕을 λ„˜μ–΄, μ‹€μ œ μ„œλΉ„μŠ€ μˆ˜μ€€μ˜ λΈ”λ‘œκ·Έ μ‹œμŠ€ν…œ κ΅¬ν˜„κ³Ό λ‹€κ΅­μ–΄ 처리 적용 λ“± 싀무에 κ°€κΉŒμš΄ μ—­λŸ‰μ„ ν•œ 단계