react-redux, RTK 라이브러리 설치
yarn add @reduxjs/toolkit
yarn add react-redux
(typesciprt인 경우 react-redux 대신 @types/react-redux
설치)
디렉토리 구조 설정
import { configureStore } from '@reduxjs/toolkit';
// import { userReducer } from '../modules/userSlice';
const store = configureStore({
reducer: { // 리듀서들 ex) user: userReducer }
});
export default store;
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>
);
// ✅ 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;
// ✅ 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를 사용하는 이유
- RTK으로 전역상태 관리하다보니, reducer의 state값은 바뀌었는데 정작 화면 렌더링은 바뀐값이 실시간 반영되지 않아서 새로고침을 해야만 새로운 값으로 반영되는 현상이 생김
-> 상태와 화면 렌더링 간의 sync를 맞춰주고 싶을 때 가장 유용하게 사용됨!
사용할 api들 정리해놓기 (getUserInfo(), updateImage() 등)
RTK때처럼 Provider 최상단에서 설정 (QueryClientProvider)
컴포넌트에서 useQuery
로 api함수 호출 (queryKey 지정)
useQueryClient
필요)mutaion
선언 (useMutation
) : 정보를 update하는 api가 mutation에 들어옴
matation.mutate()
: 정보를 update하는 api를 실행함과 동시에, queryKey로 invalidate 진행
-> 화면에 렌더링되는 값이 최신값으로 알아서 변경됨!! 🥹
🔥 react query(tanstack) 사용시 주의사항
- 오류 나기 쉬운 부분
- hook들과 isLoading 조건문, useQuery, queryClient 선언 등
순서
에 유의할 것!
(hook들 예시 : useState, useEffect, useMutation 등등 )
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;
✅ 방식 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이 실행중일때 Loading컴포넌트를 띄우고싶으나 안됨
- 해결 : mutation.isLoading했더니 안돼서
mutation.isPending
으로 바꾸니까 됐음!
useMutation
의 속성인데, react query에서 tanstack query
로 변경되면서 아래 내용이 변경됨isLoading
은 isPending
으로 바뀌었고, isPending && isFetching
의 기능인 isInitialLoading
은 isLoading
으로 변경됨(참고 : https://supersfel.tistory.com/entry/React-TanStackReact-Query-DeepDive-%ED%95%B4%EB%B3%B4%EA%B8%B0)
1) .env 환경변수에 firebase 키 설정 (vite은 .env.local)
// ⭐️ 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 (문서 삽입)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);
}
};
// ✅ 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('입력하신 값을 확인해주세요.');
});
};
// ✅ 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);
});
};
❌ 이미지 파일을 url로 변환해서 firestore에 넣고, 그걸 불러오면 될 줄 알았음
(blob 이미지 주소로 변환은 되었으나, firestore에서 다시 가져오니 화면에서 이미지 안보임)
✅파일 자체
를storage
에 저장해야함
그리고 storage에 저장된 파일의 url을 불러와서 그걸 firestore user정보에 넣어줘야함
그렇게 넣은 url은 화면에서 볼 수 있는 url이라서 이걸 가져와서 화면 업데이트하면 됨
참고
storage에 이미지 업로드 예시
// ✅ 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 });
};
📝 프로필 이미지 변경 - 내가 구현한 로직
이미지 변경을 위한 '파일 선택' 구현 위해 label, input 태그 사용
label htmlFor=어쩌구
input type="fileInput" 어쩌구
파일 선택시 화면에 보이는 이미지는 url로 바꿔서 넣어야하고, storage에 업로드할때는 파일 자체를 올려야 함
-> 업로드할 이미지랑은 state를 다르게 관리할 필요!
따라서 이미지 state는 2개
newImage - 이미지 파일을 firebase의 storage에 저장
previewImage
파일 선택시 preview에 url로 저장, 선택한 파일은 newImage에 파일 자체로 저장
updateDoc api에 인자로 newUserInfo와 newImage 전달
updateDoc api에서는
1) 이미지를 storage에 업로드하고
2) storage에 보낸 이미지의 url을 받아와서
3) firestore의 userInfo 객체에 전달하기
위의 updateDoc api를 mutation에 선언하고, '수정완료' 클릭 시 해당 mutation 실행
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);
}
};
}
문제 1. 헤더 슬라이더 이미지 안뜸
문제 2. 리스트페이지에서 로그아웃시 404
, 마이페이지에서 로그아웃시 404
vercel.json
파일 생성 필요{
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
}
command key
에 yarn dev 치니까 배포 오류남📝 추가 공부 참고
- axios : https://anywhereim.tistory.com/53
- 로딩바(loading indicator) : https://anywhereim.tistory.com/62
- tanstack query : https://anywhereim.tistory.com/61