2024.02.20 TIL - 트러블슈팅(로그인 401, 리듀서 state수정 오류, 로그인상태 유지, 이미지 preview 및 업로드)

Innes·2024년 2월 20일
0

TIL(Today I Learned)

목록 보기
69/147
post-thumbnail

🏹 트러블슈팅

  • 문제 : 로그인시 토큰 전달하여 유저정보를 GET하려는데 토큰 전달이 제대로 안됨
    (비동기 통신에서 처리 순서의 문제)

  • 로직 : 로그인 시 3가지를 실행

    • JWT 서버에 POST 메서드로 로그인 Request 보내기
      (Response로 유저정보 들어옴 - token, id, nickname, avatar, success)
    • Response로 받은 유저 정보 중 accessToken을 로컬스토리지에 저장
    • 로컬스토리지의 accessToken을 getItem, 유저정보를 JWT서버에서 GET으로 받아서 리듀서로 dispatch
      (Get으로 얻는 유저정보 : id, nickname, avatar, success)
  • 문제의 코드

// ✅ Login.jsx

// 로그인버튼 클릭 핸들러
  const onIsLoggedInHandler = async () => {
    if (isValidId && isValidPw) {
      login(userId, userPw);
      setIsLoggedIn((prevIsLoggedIn) => !prevIsLoggedIn);
    } else {
      alert("입력하신 값을 확인해주세요.");
    }
    
	// 로그인 유저정보 리듀서에 전달하기
      const accessToken = localStorage.getItem("loggedInUserToken");
      const loggedInUserInfo = await getLoggedInUserInfo(accessToken);

      dispatch(addUser({ loggedInUserInfo, accessToken }));
  };
// ✅ users.js

// 로그인
export const login = async (userId, userPw) => {
  const response = await axios.post(
    "https://moneyfulpublicpolicy.co.kr/login",
    { id: userId, password: userPw }
  );

  // 로그인시 로컬스토리지에 토큰 저장하기
  const loggedInUserInfo = response.data;
  const loggedInUserToken = loggedInUserInfo.accessToken;
  localStorage.setItem("loggedInUserToken", loggedInUserToken);
};

// 로그인된 유저정보 가져오기
export const getLoggedInUserInfo = async (token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  };
  const response = await axios.get(
    "https://moneyfulpublicpolicy.co.kr/user",
    config
  );
  return response.data;
};
  • 시도

    • getLoggedInUserInfo() 앞에 await 수정
    • 로그아웃시 localstorage.clear()의 문제인가 싶어 수정
  • 해결

    • 유저정보를 받아와서 dispatch하는 부분을 else문 위로 올림
    • login() 부분에도 await 붙이기

      결국 비동기 처리순서의 문제가 맞았음. login() 앞에 await붙이는게 중요한 해결 포인트였음

// ✅ Login.jsx
// 로그인버튼 클릭 핸들러
  const onIsLoggedInHandler = async () => {
    if (isValidId && isValidPw) {
      await login(userId, userPw);
      setIsLoggedIn((prevIsLoggedIn) => !prevIsLoggedIn);

      // 로그인 유저정보 리듀서에 전달하기
      const accessToken = localStorage.getItem("loggedInUserToken");
      const loggedInUserInfo = await getLoggedInUserInfo(accessToken);

      dispatch(addUser({ loggedInUserInfo, accessToken }));
    } else {
      alert("입력하신 값을 확인해주세요.");
    }
  };

  • 문제 : 새로고침시 계속 letter가 중복으로 덧붙는 문제
    리듀서로 받아오는 letters가 계속 배열 속의 배열 혹은 8개 16개 이런식으로 정확한 db 객체배열을 반환하지 못함

  • 시도 : data, letters를 계속 콘솔에 찍어보며 await의 위치, 의존성 배열 값 수정, dispatch로 보내는 인자를 [data], [...data] 등 계속 바꿔보기, 리듀서의 return값을 [newLetter, ...state], [...newLetter, ...state] 등 계속 수정을 거듭함

  • 원인 : 리듀서의 반환값에서 state와 newletter의 순서에서 생긴 오류!

  • 해결


// ✅ 컴포넌트
  useEffect(() => {
    const letters = async () => {
      const data = await getLetters();
      dispatch(addLetter(data));
    };
    letters();
  }, [dispatch]);

  const filteredLetters = letters.filter(
    (letter) => letter.writedTo === activeMember
  );

// ✅ 리듀서
const initialState = [];

const lettersSlice = createSlice({
  name: "letters",
  initialState: initialState,
  reducers: {
    addLetter: (state, action) => {
      const newLetter = action.payload;
      return [...state, ...newLetter];
    },
    deleteLetter: (state, action) => {
      const letterId = action.payload;
      return state.filter((letter) => letter.id !== letterId);
    },
    editLetter: (state, action) => {
      const { id, editingText } = action.payload;
      return state.map((letter) => {
        if (letter.id === id) {
          return { ...letter, content: editingText };
        }
        return letter;
      });
    },
  },
});

  1. map 에서 key의 중복으로 인한 오류
    사실 key중복은 그냥 결과로 나오는 오류였을 뿐, 원인은 아니었음
    원인은 dispatch로 addLetter에 getLetter한 letter들을
    렌더링할때마다 붙여주는데,
    렌더링할때마다 계속 있는걸 또 붙이고 또 붙이니까 뒤에 자꾸 있는게 또붙고 또붙고...

렌더링할때 내가 정말 원했던건 계속 붙여주는 게 아니라, 페이지 렌더링이 됐을때 가장 최신상태의 letter들을 get하는거였다
useEffect의 의존성배열을 잘못 설정한줄알고 그 부분만 계속 수정하면서 슈팅했는데, 그게 아니라 addLetter가 아닌 letter를 초기화하는 action creator를 새로 만들어줘야하는 거였다!!!

payload로 get한 최신 letter들을 넘겨주고, state를 action.payload로 만들어주면 최신화하는 action creator 완성!!

  1. Outlet 은 부모컴포넌트 return문 안에 넣어주기
    Outlet 대신에 라우터 상 자식 컴포넌트들이 하나씩 붙는것
    그래서 Outlet이 들어있는 부모컴포넌트는 어느 페이지를 가나 공통적으로 보여지는것
    -> header에 Outlet을 넣어서 헤더의 페이지들을 Outlet 대신 붙게끔

근데 그 Outlet이 들어있는 컴포넌트 안에서
useEffect로 토큰이 있으면 보여주고, 없으면 로그인페이지로
이동하게 만들면 로그인상태 유지하는것처럼 보여질 수 있음

  1. 로그인상태 유지하기
    시도 1. router에서 token 없으면 로그인컴포넌트로, 있으면 authlayout, home, ... 컴포넌트로 가게 만들었는데
    token이 바뀌어도 페이지가 자동으로 이동하지 않음
    (token이 바뀌면 자동으로 리렌더링되게 만들고싶음)

시도2. 로그아웃하면 navigate로 login페이지로 넘어가게 했는데 말 안들음
(빈화면으로 나옴)

시도3. authlayout에서 useEffect로 token체크하고 없으면 login으로 navigate 줘도 안됨
(새로고침하면 로그인화면 잘 나옴)

발생하고 있는 문제

  • 로그인 클릭시 서버 로그인, 유저정보 가져오기, 토큰 로컬스토리지에 붙이기 는 다 되는데, 화면이 로그인화면으로 유지됨
    -> 새로고침하면 홈화면 나옴

  • 로그아웃하면 로컬스토리지 토큰은 없어지는데 화면은 로그인화면으로 전환되지 않음

원하는것 : 새로고침할때마다, 로그인버튼 클릭시, 로그아웃버튼 클릭시 token유무를 파악해서 있으면 authlayout 보이게, 없으면 login화면으로 나오게 만들고싶음
(navigate 이용하지 않고 알아서 token 유무 파악하게 할수 없나?)

혼자 생각해본것

  • 새로고침했을때 로컬스토리지 토큰이 없어졌다는걸 알려주려면 useEffect로 해야될거같은데 어디에 줘야되지...?
  • 로그인, 로그아웃 클릭시 로컬스토리지 토큰이 생기고 없어지는 상태변화를 알려주려면 onclick에다가 어떤 로직을 넣어줘야되는게 맞나...?

👍🏻 해결

  • 라우터에서 토큰을 로컬스토리지 토큰을 가지고 유무를 판별해서 화면 렌더링하는 로직으로 만들었는데
    -> 판별할 토큰을 로컬스토리지에 있는거 말고 리듀서에 넣어놓은 유저 토큰으로 판별하면 됨!
    (로컬스토리지 토큰은 아무리 변해도 리액트가 인식하지 못함.
    리듀서의 state가 변하는건 리액트가 인식하기 때문에 state에 들어있는 token으로 판별하기)

-> 화면이 리렌더링되면 리듀서의 state값은 초기값으로 변함
(리듀서의 state는 서버에 저장한게 아니기때문에 새로고침하면 날아가는것)

-> 새로고침하면 날아가니까 라우터에서 useEffect로 로그인때 했던것처럼 그대로 유저정보랑 토큰을 state에 넣어주는 로직 넣기

-> 새로고침해도 state에 토큰이 남아있기 때문에(전 단계 로직으로 인해) 토큰으로 판별시 로그인상태로 인식하여 홈화면을 유지할 수 있음

-> 로그아웃 클릭시, 리듀서의 state를 초기값으로 초기화(초기값으로 state를 바꿔주는 action creator 추가)
(토큰값 없어지므로 화면이 로그인화면으로 전환됨)

-> 이렇게하면 굳이 로그인, 로그아웃 버튼 클릭시 navigate로 페이지 이동하게끔 하드코딩하지 않아도 state 변경에 따라 알아서 화면을 전환한다!!
(리액트에서 리렌더링 조건 3가지 : state의 변화, props 변화, 부모 컴포넌트 변화 중 state변화에 해당)


Detail 상세페이지에서 해당 letter 삭제하기 중 오류

  • 문제 : 삭제 후 홈으로 화면 이동 직후에도 해당 id의 카드를 find하면서 오류 발생
  • 시도
    1) Detail컴포넌트에 useEffect로 최신의 letters 가져오게하기
    2) letter값이 없는경우 "/"로 이동하고 return null로 주기
    (정상 동작은 되지만 콘솔창에 warning 뜸)
  • 해결
    1) Detail컴포넌트 시작시 useEffect로 해당 id에 해당하는 letter를 찾게하고,
    2) 찾은 letter를 useState로 관리, set해주기
    3) letter가 없으면 return null 하게 한 후,
    4) 해당 letter의 key값을 가져오기
// ✅ Detail.jsx - 선택한 letter의 상세페이지

export default function Detail() {
  const dispatch = useDispatch();
  const letters = useSelector((state) => state.letters);
  const [foundLetter, setFoundLetter] = useState({});

  const navigate = useNavigate();
  const { id } = useParams();

  useEffect(() => {
    // ⭐️ 1)
    const selectedLetter = letters.find((letter) => letter.id === id);
    // ⭐️ 2)
    setFoundLetter(selectedLetter);
  }, [id, letters]);

  // ⭐️ 3)
  if (!foundLetter) return null;

  // ⭐️ 4)
  const { avatar, nickname, createdAt, writedTo, content } = foundLetter;

이미지 preview 및 업로드

참고

중요한점

  • label의 id와 input의 id가 같아야 input이 보이지 않더라도 둘이 연결되어 이미지 클릭해도 파일선택 되도록 만들수 있는 것!!
profile
꾸준히 성장하는 우상향 개발자

0개의 댓글