2024.02.29 TIL - Redux Toolkit 사용법, React query(tanstack)정리, mutation 사용법 2가지, mutation.isPending, firebase(firestore, storage), vercel 오류

Innes·2024년 2월 28일
0

TIL(Today I Learned)

목록 보기
77/147
post-thumbnail

RTK (Redux Toolkit) 사용법

  1. react-redux, RTK 라이브러리 설치
    yarn add @reduxjs/toolkit
    yarn add react-redux
    (typesciprt인 경우 react-redux 대신 @types/react-redux 설치)

  2. 디렉토리 구조 설정

  • redux
    • config / configStore.js
    • modules / userSlice.js (사용하는 slice이름 지정)
  1. store 설정 - configStore.js
import { configureStore } from '@reduxjs/toolkit';
// import { userReducer } from '../modules/userSlice';

const store = configureStore({
  reducer: { // 리듀서들 ex) user: userReducer }
});

export default store;
  1. Provider 설정 - App.jsx 혹은 기타 최상위 컴포넌트 (vite은 main.jsx)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { Provider } from 'react-redux';
import store from './redux/config/configStore.js';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
      <Provider store={store}>  // ⭐️ Provider로 store를 전역상태화
        <App />
      </Provider>
  </React.StrictMode>
);
  1. Slice 작성 - userSlice.js
// ✅ userSlice.js - Utrend 프로젝트

import { createSlice } from '@reduxjs/toolkit';

const initialState = {};
const UserSlice = createSlice({
  name: 'user',   // Slice 이름
  initialState,
  reducers: {     // defaultUser 등 : action creator이자 reducer
    defaultUser: (state, action) => {
      return action.payload;
    },
    updateNickname: (state, action) => {
      return { ...state, nickname: action.payload };
    },
    updateIntro: (state, action) => {
      return { ...state, intro: action.payload };
    },
  }
});

export const userReducer = UserSlice.reducer;
export const { defaultUser, updateNickname, updateIntro } = UserSlice.actions;
  1. 컴포넌트에서 사용하기
// ✅ Header.jsx - Utrend 프로젝트

import { useDispatch, useSelector } from 'react-redux';
import { login } from '../../redux/modules/loginSlice';

const Header () => {
	  // login상태 RTK에서 가져오기
  // useSelector : reducer로 state값을 가져오게 해줌
  const loginState = useSelector((state) => state.loginReducer);
  const dispatch = useDispatch();
  
  const onLogoutHandler = () => {
    dispatch(login(false));	  // ⭐️ dispatch로 reducer에 값 전달
  };
  
  return (
  <div>{loginState? ('A') : ('B')}</div>
  )
}

export default Header;


React Query 사용법(tanstack 기준)

📝 내가 생각하는 react query를 사용하는 이유

  • RTK으로 전역상태 관리하다보니, reducer의 state값은 바뀌었는데 정작 화면 렌더링은 바뀐값이 실시간 반영되지 않아서 새로고침을 해야만 새로운 값으로 반영되는 현상이 생김
    -> 상태와 화면 렌더링 간의 sync를 맞춰주고 싶을 때 가장 유용하게 사용됨!

1. 사용법 간단 정리

  • 사용할 api들 정리해놓기 (getUserInfo(), updateImage() 등)

  • RTK때처럼 Provider 최상단에서 설정 (QueryClientProvider)

  • 컴포넌트에서 useQuery로 api함수 호출 (queryKey 지정)

    • 정보가 변경되는 경우, 이 함수로 가져온 값을 queryKey를 통해 무효화시킬 것임
      (invalidtate할때, useQueryClient 필요)
      -> 그 다음 값, 즉 최신값으로 다시 가져오게 할 것
  • mutaion 선언 (useMutation) : 정보를 update하는 api가 mutation에 들어옴

  • matation.mutate() : 정보를 update하는 api를 실행함과 동시에, queryKey로 invalidate 진행
    -> 화면에 렌더링되는 값이 최신값으로 알아서 변경됨!! 🥹

🔥 react query(tanstack) 사용시 주의사항

  • 오류 나기 쉬운 부분
    • hook들과 isLoading 조건문, useQuery, queryClient 선언 등 순서에 유의할 것!
      (hook들 예시 : useState, useEffect, useMutation 등등 )

2. 사용법

1) 최상단 컴포넌트에서 QueryClientProvider 설정 - vite은 main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>
  </React.StrictMode>
);

2) 컴포넌트에서 useQuery, useMutation, mutation.mutate

// ✅ MyProfile.jsx - Utrend 프로젝트
// (query 관련 부분만)

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (data) => {
      const { uid, newUserInfo, newImage } = data;
      await updateUser(uid, newUserInfo, newImage);
    },
    onSuccess: () => {
      queryClient.invalidateQueries(['userInfo']);
    }
  });

  const { isLoading, isError, data } = useQuery({
    queryKey: ['userInfo'],
    queryFn: async () => {
      const res = await getUserInfo(uid);
      setNewNickname(res.nickname);
      setNewIntro(res.intro);
      setNewImage(res.image);
      setPreviewImage(res.image);
      return res;
    }
  });

  if (isLoading || mutation.isPending) {
    return <Loading />;
  }
  if (isError) {
    return <Error />;
  }

  const { userId, nickname, image, intro } = data;


Mutation 사용법 2가지

✅ 방식 1. mutation으로 정의 후 mutation.mutate()로 사용

// mutation 정의
// (query로 newUserInfo update, invalidate(optimistic UI) 하기)
  const mutation = useMutation({
    mutationFn: (newUserInfo) => updateUserInfo(uid, newUserInfo),
    onSuccess: () => {
      queryClient.invalidateQueries(['userInfo']);
    }
  });

// mutation 사용
// (수정된 정보 query로 전달하기)
      mutation.mutate(newUserInfo);

✅ 방식 2. mutation에 이름 붙이기, 붙여준 이름으로 mutation사용하기

// mutation 정의
// (query로 newUserInfo update, invalidate(optimistic UI) 하기)
  const { mutate: updateMutation } = useMutation({
    mutationFn: (newUserInfo) => updateUserInfo(uid, newUserInfo),
    onSuccess: () => {
      queryClient.invalidateQueries(['userInfo']);
    }
  });

// mutation 사용
// (수정된 정보 query로 전달하기)
updateMutation(newUserInfo);


mutation.isLoading? isPending?

🏹 트러블 슈팅

  • 문제 : mutation이 실행중일때 Loading컴포넌트를 띄우고싶으나 안됨
  • 해결 : mutation.isLoading했더니 안돼서 mutation.isPending으로 바꾸니까 됐음!
  • 둘다 useMutation의 속성인데, react query에서 tanstack query로 변경되면서 아래 내용이 변경됨
    • 기존의 isLoadingisPending으로 바뀌었고,
    • isPending && isFetching의 기능인 isInitialLoadingisLoading으로 변경됨

(참고 : https://supersfel.tistory.com/entry/React-TanStackReact-Query-DeepDive-%ED%95%B4%EB%B3%B4%EA%B8%B0)



firebase 사용하기

firebase 설정하기

1) .env 환경변수에 firebase 키 설정 (vite은 .env.local)

  • firebase CDN에서 복사해온 값 -> 민감한 개인정보이기 때문에 .env에 숨겨주자
// ⭐️ vite인 경우 앞이 VITE_ 로 시작해야 함
// CRA인 경우 REACT_APP_
VITE_API_FIRESTORE_KEY = ""
VITE_APP_FIRESTORE_AUTHDOMAIN = ""
VITE_APP_FIRESTORE_PROJECTID = ""
VITE_APP_FIRESTORE_STORAGEBUCKET = ""
VITE_APP_FIRESTORE_MESSAGINGSENDERID = ""
VITE_APP_FIRESTORE_APPID = ""

2) config.js 파일에 firebase 초기화 설정

// use정보 업데이트를 위해 authentication과 storage 사용중 참고

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore/lite';
import { getAuth } from 'firebase/auth';   // athentication
import { getStorage } from 'firebase/storage';   // storage

const {
  // ⭐️ vite인 경우 앞이 VITE_ 로 시작해야 함
  // CRA인 경우 REACT_APP_
  VITE_API_FIRESTORE_KEY,
  VITE_APP_FIRESTORE_AUTHDOMAIN,
  VITE_APP_FIRESTORE_PROJECTID,
  VITE_APP_FIRESTORE_STORAGEBUCKET,
  VITE_APP_FIRESTORE_MESSAGINGSENDERID,
  VITE_APP_FIRESTORE_APPID
} = import.meta.env;    
	// ⭐️ vite인 경우 import.meta.env;
	// CRA인 경우 process.env;

const firebaseConfig = {
  apiKey: VITE_API_FIRESTORE_KEY,
  authDomain: VITE_APP_FIRESTORE_AUTHDOMAIN,
  projectId: VITE_APP_FIRESTORE_PROJECTID,
  storageBucket: VITE_APP_FIRESTORE_STORAGEBUCKET,
  messagingSenderId: VITE_APP_FIRESTORE_MESSAGINGSENDERID,
  appId: VITE_APP_FIRESTORE_APPID
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export default db;
export const auth = getAuth(app);  // authentication
export const storage = getStorage(app); // storage

3) fireStore에 CRUD 구현

  • addDoc : Create (문서 삽입)
    (⭐️ 문서 id 지정해서 넣고싶을땐 setDoc으로)
  • getDoc : Read (문서 조회)
  • updateDoc : Update (문서 수정)
  • deleteDoc : Delete (문서 삭제)

4) api 사용 예시

// ✅ auth.js - Utrend 프로젝트

import { doc, getDoc, setDoc, updateDoc } from 'firebase/firestore/lite';
import db from './config';
import { storage } from './config';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';

export const addDefaultUserInfo = async (uid, newUserInfo) => {
  // ⭐️ setDoc - uid를 문서id로 하는 문서 생성
  // ⭐️ addDoc - uid없이 쓸 경우, 자동 생성된 문서id로 문서 생성
  // 'users' : 컬렉션 이름'
  //  newUserInfo : 문서에 추가할 객체값
  try {
    await setDoc(doc(db, 'users', uid), newUserInfo);
  } catch (error) {
    console.error(error);
  }
};

export const addGoogleUserInfo = async (uid, newUserInfo) => {
  // ⭐️ getDoc - uid를 문서이름으로 하는 문서 조회
  // 양식 : await getDoc(doc(db, 'users', uid))
  try {
    const userDocRef = doc(db, 'users', uid);
    const userDocSnapshot = await getDoc(userDocRef);

    if (!userDocSnapshot.exists()) {
      await setDoc(doc(db, 'users', uid), newUserInfo);
    }
  } catch (error) {
    console.error(error);
  }
};

export const getUserInfo = async (uid) => {
  try {
    // ⭐️ getDoc - uid를 문서이름으로 하는 문서 조회
    // return 받아야 함(조회할거니까 반환 값을 받아야 함)
    const userInfo = await getDoc(doc(db, 'users', uid));
    return userInfo.data(); // ⭐️ .data 유의
  } catch (error) {
    console.error(error);
    return null;
  }
};

// 참고 : storage, fireStore에 이미지 업로드
// 주의 : storage에는 파일로, fireStore에는 URL로 업로드 해야함
export const updateImage = async (uid, uploadedImage) => {
  // storage에서 이미지 넣기
  const uploadedFile = await uploadBytes(ref(storage, `images/${uploadedImage.name}`), uploadedImage);
  // 해당 이미지의 url받기
  const file_url = await getDownloadURL(uploadedFile.ref);
  // url을 fireStore에 넣기
  await updateDoc(doc(db, 'users', uid), { image: file_url });
};

// 참고 : fireStore에 newUserInfo 객체 넣기
export const updateUser = async (uid, newUserInfo, newImage) => {
  try {
    const { nickname, intro } = newUserInfo;
    const uploadedFile = await uploadBytes(ref(storage, `images/${newImage.name}`), newImage);
    const imageURL = await getDownloadURL(uploadedFile.ref);

    await updateDoc(doc(db, 'users', uid), { nickname, intro, image: imageURL });
  } catch (error) {
    console.error('error', error);
  }
};

firebase authentication

1. 회원가입

// ✅ SignUp.jsx - Utrend 프로젝트

import { createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../../api/config';

  const onSignUpHandler = () => {
    // ⭐️ 회원가입 : createUserWithEmailAndPassword(auth, userId, userPw)
    createUserWithEmailAndPassword(auth, userId, userPw)
      .then((userCredential) => {
        console.log(userCredential);
        alert('회원가입이 완료되었습니다 🎉');
        setIsSignUpOpen((prev) => !prev);

        // 회원가입시 userInfo를 fireStore에 저장
        const { uid } = userCredential.user;
        const newUserInfo = {
          uid,
          userId,
          nickname: userNickname,
          image: null,
          favChannels: [],
          intro: '소개를 입력해주세요.'
        };
        addDefaultUserInfo(uid, newUserInfo);
      })
      .catch((error) => {
        console.error(error);
        alert('입력하신 값을 확인해주세요.');
      });
  };

2. 로그인

// ✅ Login.jsx - Utrend 프로젝트

import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../../api/config';

  const onLoginHandler = async () => {
    try {
      const userCredential = await 
      // ⭐️ 로그인 : signInWithEmailAndPassword(auth, userId, userPw)
      signInWithEmailAndPassword(auth, userId, userPw);
      alert('로그인 되었습니다.');
      setIsLoginOpen((prev) => !prev);

      // 로그인시 sesstionStorage에 uid, userId 저장
      sessionStorage.setItem('userId', userId);
      sessionStorage.setItem('uid', userCredential.user.uid);

      // 로그인 상태를 boolean값으로 전역상태 관리하는 RTK에 전달
      dispatch(login(true));

      setUserId('');
      setUserPw('');
    } catch (error) {
      console.log(error);
      alert('입력하신 값을 확인해주세요.');
    }
  };

소셜 로그인 - 구글

// ✅ Login.jsx - Utrend 프로젝트
// 소셜 로그인 중 '구글 로그인' 구현

import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { auth } from '../../api/config';

  const onGoogleLogin = () => {
    setIsLoginOpen((prev) => !prev);
    const provider = new GoogleAuthProvider();
    
    // 구글로그인 팝업 열어서 로그인 진행
    signInWithPopup(auth, provider)
      .then((userData) => {
        alert('로그인 되었습니다.');

        // sessionStorage에 uid, userId 저장
        sessionStorage.setItem('userId', userData.user.email);
        sessionStorage.setItem('uid', userData.user.uid);

        // 로그인상태 Boolean값을 전역상태 관리하는 RTK에 전달
        dispatch(login(true));

        // fireStore에 해당 uid를 문서id로 하는 문서가 없으면
        //  -> userInfo를 fireStore에 저장
        // (구글로그인은 회원가입 로직이 없으므로 여기서 fireStore에 저장해줌) 
        const { uid } = userData.user;
        const newUserInfo = {
          uid,
          userId: userData.user.email,
          nickname: userData.user.displayName,
          image: null,
          favChannels: [],
          intro: '소개를 입력해주세요.'
        };

        addGoogleUserInfo(uid, newUserInfo);
      })
      .catch((error) => {
        console.log(error);
      });
  };

storage를 이용한 이미지 업로드

❌ 이미지 파일을 url로 변환해서 firestore에 넣고, 그걸 불러오면 될 줄 알았음
(blob 이미지 주소로 변환은 되었으나, firestore에서 다시 가져오니 화면에서 이미지 안보임)


파일 자체storage에 저장해야함
그리고 storage에 저장된 파일의 url을 불러와서 그걸 firestore user정보에 넣어줘야함
그렇게 넣은 url은 화면에서 볼 수 있는 url이라서 이걸 가져와서 화면 업데이트하면 됨

// ✅ auth.js - Utrend 프로젝트

import { updateDoc } from 'firebase/firestore/lite';
import db from './config';
import { storage } from './config';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';

// ⭐️ storage, fireStore에 이미지 업로드
export const updateImage = async (uid, uploadedImage) => {
  // storage에서 이미지 넣기
  const uploadedFile = await uploadBytes(ref(storage, `images/${uploadedImage.name}`), uploadedImage);
  // 해당 이미지의 url받기
  const file_url = await getDownloadURL(uploadedFile.ref);
  // url을 fireStore에 넣기
  await updateDoc(doc(db, 'users', uid), { image: file_url });
};

📝 프로필 이미지 변경 - 내가 구현한 로직

  1. 이미지 변경을 위한 '파일 선택' 구현 위해 label, input 태그 사용
    label htmlFor=어쩌구
    input type="fileInput" 어쩌구

  2. 파일 선택시 화면에 보이는 이미지는 url로 바꿔서 넣어야하고, storage에 업로드할때는 파일 자체를 올려야 함

-> 업로드할 이미지랑은 state를 다르게 관리할 필요!
따라서 이미지 state는 2개
newImage - 이미지 파일을 firebase의 storage에 저장
previewImage

파일 선택시 preview에 url로 저장, 선택한 파일은 newImage에 파일 자체로 저장

  1. updateDoc api에 인자로 newUserInfo와 newImage 전달
    updateDoc api에서는
    1) 이미지를 storage에 업로드하고
    2) storage에 보낸 이미지의 url을 받아와서
    3) firestore의 userInfo 객체에 전달하기

  2. 위의 updateDoc api를 mutation에 선언하고, '수정완료' 클릭 시 해당 mutation 실행

  3. usequery로 불러오던 getDoc api를 invalidate 시켜서 화면이 새로운 userInfo로 업데이트됨

// ✅ MyProfile.jsx - Utrend 프로젝트

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getUserInfo, updateUser } from '../../api/auth';

const MyProfile = () => {
  // ... 생략
  const queryClient = useQueryClient();

  // fireStore에 newUserInfo update
  const mutation = useMutation({
    mutationFn: async (data) => {
      const { uid, newUserInfo, newImage } = data;
      await updateUser(uid, newUserInfo, newImage);
    },
    // 'useInfo' queryKey의 함수로 불러온 값 무효화, 최신값 불러오기
    onSuccess: () => {
      queryClient.invalidateQueries(['userInfo']);
    }
  });

  // fireStore에서 userInfo 가져오기
  const { isLoading, isError, data } = useQuery({
    queryKey: ['userInfo'],
    queryFn: async () => {
      const res = await getUserInfo(uid);
      setNewNickname(res.nickname);
      setNewIntro(res.intro);
      setNewImage(res.image);
      setPreviewImage(res.image);
      return res;
    }
  });

  // 로딩중이거나, mutation이 실행중이면 Loading컴포넌트 보여주기
  if (isLoading || mutation.isPending) {
    return <Loading />;
  }
  if (isError) {
    return <Error />;
  }
  // ...생략
  
  // 수정버튼 클릭시 mutation 실행
  const onUpdateUserInfo = async () => {
    const isConfirmed = window.confirm('수정하시겠습니까?');
    if (isConfirmed) {
      const newUserInfo = {
        nickname: newNickname,
        intro: newIntro
      };

      mutation.mutate({ uid, newUserInfo, newImage });

      setIsEdit(false);
    }
  };
  
}


vercel 배포오류

  • 문제 1. 헤더 슬라이더 이미지 안뜸

    • 이미지 경로가 src안에 상대경로로 들어있었음. 이미지를 import로 받아서 써야됨
  • 문제 2. 리스트페이지에서 로그아웃시 404, 마이페이지에서 로그아웃시 404

    • 새로고침 시 생기는 오류였음. 루트에 vercel.json 파일 생성 필요
{
  "rewrites": [{ "source": "/(.*)", "destination": "/" }]
}
  • 문제 3. vercel에 배포과정 중 env key, value 수기 입력하고, command key에 yarn dev 치니까 배포 오류남
    -> command key는 비워두는게 맞았음



📝 추가 공부 참고

profile
꾸준히 성장하는 우상향 개발자

0개의 댓글