Skip to content

트러블 슈팅 (Trouble Shooting)

JaeJune Ryu edited this page Jul 31, 2022 · 10 revisions

개발하면서 있었던 이슈와 고민해서 해결한 방법을 기록합니다.

1. useEffect의 특성 및 비동기적 처리 Issue

  • 문제 상황
    • useEffect의 의존성 배열을 사용하여 해당 변수가 변할 때만 렌더링을 시키려고 할 때, useEffect 특성상 마운트될 때도 실행이 되어 문제가 발생하였습니다.
    • 해당 코드에서 마운트될 때 토큰 유효성을 검증하고, 상태가 변하면 그에 맞게 페이지 이동을 기대했습니다.
    • 그러나, 밑에 있는 useEffect가 마운트될 때에도 실행이 되어 Welcome 페이지를 거치고 피드 페이지로 이동하는 문제가 발생하였습니다.
const [isTokenValid, setIsTokenValid] = useState(false);

// 마운트 될 때 토큰 유효성 검증 + 유효성 상태 변경
useEffect(() => {
    (async function checkTokenAvailable() {
      try {
        const accessToken = localStorage.getItem('accessToken');
        const res = await axios.get('https://mandarin.api.weniv.co.kr/user/checktoken', {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });
        (await res.data.isValid) && setIsTokenValid((state) => !state);
      } catch (error) {
        console.log(error);
      }
    })();
 }, []);

// 토큰 유효 상태가 변하면 상태에 맞게 페이지 이동
useEffect(() => {
  setTimeout(() => {
    if (localStorage.getItem('accessToken') && isTokenValid) {
      history.push('/home-empty');
    } else {
      history.push('/welcome');
    }
  }, 2000);
}, [isTokenValid]);
  • 해결 방법
    1. 따라서, useEffect가 오직 의존성 배열 내부의 상태가 변할때만 실행되도록 코드를 수정했습니다.
const mount = useRef();

  const [isTokenValid, setIsTokenValid] = useState(false);

  useEffect(() => {
    (async function checkTokenAvailable() {
      try {
        const accessToken = localStorage.getItem('accessToken');
        const res = await axios.get('https://mandarin.api.weniv.co.kr/user/checktoken', {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        });
        (await res.data.isValid) && setIsTokenValid((state) => !state);
      } catch (error) {
        console.log(error);
        setTimeout(() => {
          history.push('/welcome');
        }, 2000);
      }
    })();
  }, []);

  useEffect(() => {
    if (!mount.current) {
      mount.current = true;
    } else {
      setTimeout(() => {
        if (localStorage.getItem('accessToken') && isTokenValid) {
          history.push('/home-empty');
        }
      }, 2000);
    }
  }, [isTokenValid]);
  1. useEffect의 비동기적 특성을 해결하기 위해서 useLayoutEffect 훅을 활용하였습니다.
useLayoutEffect(() => {
    setTimeout(() => {
      if (localStorage.getItem('accessToken') && isTokenValid) {
        history.push('/home-empty');
      }
    }, 2000);
}, [isTokenValid]);

2. setState 함수의 비동기 처리 방식

  • 문제 상황
    • 아이디, 비밀번호 Input 태그 안에 모두 값이 들어가야 버튼을 활성화시키는 기능을 구현 도중에 문제가 발생했습니다.
    • 다음과 같은 코드에서 setState 함수가 비동기적으로 실행이 되어서 동작이 한 단계씩 밀리는 문제가 생겼습니다.
    • 아이디에 값을 입력하고, 비밀번호에 2글자를 입력해야 버튼이 활성화되는 문제가 발생했습니다.
const [id, setId] = useState('');
const [password, setPassword] = useState('');
const [isEmpty, setIsEmpty] = useState(true);

// ID, PW 동적으로 업데이트
const onHandleUserId = (e) => {
  setId(e.target.value);
};

const onHandleUserPassword = (e) => {
  setPassword(e.target.value);
};

if (id && password) {
  setIsEmpty(false);
} else {
  setIsEmpty(true);
}
  • 해결 방법
    • 따라서, useEffect를 이용하여 id, password가 변할때만 작동하도록 구현하였습니다.
useEffect(() => {
  if (id && password) {
    setIsEmpty(false);
  } else {
    setIsEmpty(true);
  }
}, [id, password]);

3. Redux-persist

  • 문제 상황
    • Redux Store에서 유저정보를 관리하는데, 새로고침 시 전역으로 관리하고 있는 상태 데이터가 초기화되는 문제가 발생하였습니다.
  • 해결 방법
    • Redux Store와 로컬 스토리지를 연결해주는 Redux-persist 라이브러리를 도입하여 이를 방지했습니다.
// src/store/index.js

import { combineReducers } from 'redux';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import UserInfoReducer from './UserInfo';
import PostInfoReducer from './PostInfo';
import OtherUserInfoReducer from './OtherUserInfo';
import ProductInfoReducer from './ProductInfo';

const persistConfig = {
  key: 'UserInfo',
  storage,
  whitelist: ['UserInfoReducer', 'PostInfoReducer', 'OtherUserInfoReducer', 'ProductInfoReducer'],
};

const rootReducer = combineReducers({
  UserInfoReducer,
  PostInfoReducer,
  OtherUserInfoReducer,
  ProductInfoReducer,
});

export default persistReducer(persistConfig, rootReducer);
// src/index.js
const root = ReactDOM.createRoot(document.getElementById('root'));

const store = createStore(rootReducer);
const persistor = persistStore(store);

root.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

4. UX (사용자 경험)

  • 문제 상황
    • 서버와의 통신을 통해서 데이터를 받은 뒤에 화면에 렌더링을 하기 때문에 사용자 경험 측면에서 나쁜 문제가 발생했습니다.
  • 해결 방법
    • 당장 렌더링 최적화에 대한 공부는 늦었다고 생각해서 로딩 중일때 사용자에게 알리는 컴포넌트를 만들어 사이에 렌더링했습니다.
    • 렌더링 최적화에 대한 공부를 진행하여 추후에 리팩토링 예정입니다.

5. 데이터 업데이트 시점

  • 문제 상황
    • 하단 탭 메뉴를 클릭해 프로필 페이지로 이동 시 데이터를 요청해 받아 올 수 있도록 버튼 동작으로 서버 요청 로직을 작성하였습니다.
    • 게시글 업로드 및 삭제 시 변경된 게시글 정보가 바로 업데이트되지 않고 다른 페이지로 이동했다 돌아와야만 새로운 데이터 정보가 렌더링되는 문제가 발생하였습니다.
    • TabMenu에서 프로필, 게시글, 상품 정보를 불러오고 있어 탭 메뉴를 클릭할 때만 서버로부터 데이터를 받아 오고 있었습니다.
  • 해결 방법
    • 게시글 정보를 불러오는 컴포넌트로 함수를 옮겨 데이터의 변경이 있을 때마다 업데이트되도록 구현하였습니다.

6. 프로필 페이지 렌더링

  • 문제 상황
    • 위에서 데이터를 업데이트해 오는 시점은 정상적으로 맞춰졌으나, 게시글 정보를 가지고 오는 배열인 postList가 undefined 값을 가지며 데이터를 불러오지 못하는 문제가 발생하였습니다.
    • 한 컴포넌트 내에서 useSelector와 dispatch를 한 번에 동작시켜 dispatch 이전에 선언된 useSelector가 먼저 동작하며 빈 배열을 가지고 오게 되었습니다.
const { UserAccount } = useSelector((state) => state.UserInfoReducer);
const { postList } = useSelector((state) => ({
  postList: state.PostInfoReducer.postList
}));

useEffect(() => {
  getPost();
}, []);

const getPost = async () => {
  try {
    const accessToken = localStorage.getItem("accessToken");
    const res = await axios.get(
      `https://mandarin.api.weniv.co.kr/post/${UserAccount}/userpost`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-type": "application/json"
        }
      }
    );
    const postList = res.data;
    dispatch({ type: "GET_POST", postList });
  } catch (error) {
    console.log(error);
  }
};
  • 해결 방법
    • 부모 컴포넌트인 UserProfile에서 dispatch로 리듀서 함수를 동작시키고, 자식 컴포넌트인 PostCard에서 useSelector를 사용해 게시글 데이터를 가져오도록 분리하였습니다.
// PostCard.jsx
const dispatch = useDispatch();

useEffect(() => {
  (async function getProductAndPost() {
    try {
      const accessToken = localStorage.getItem("accessToken");
      const res = await axios.get(
        `https://mandarin.api.weniv.co.kr/post/${UserAccount}/userpost`,
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-type": "application/json"
          }
        }
      );
      const postList = res.data.post;
      dispatch({ type: "GET_POST", postList });
    } catch (error) {
      console.log(error);
    }
  })();
}, []);
// UserProfile.jsx
const { postList } = useSelector((state) => ({
    postList: state.PostInfoReducer.postList,
}));

7. state 값 변경

  • 문제 상황
    • 게시글 수정 페이지로 이동 시 기존 작성 내용을 화면에 불러오고 싶었으나 모든 게시글 수정 화면에서 가장 최근에 작성된 게시글 정보를 불러오는 문제가 발생하였습니다.
    • 현재 게시글의 postId로 서버에 데이터를 요청해 정상 응답은 받고 있었으나 렌더링이 안 되는 상황이었습니다.
    • state 값을 직접 변경하려고 시도해 로직이 올바르게 동작하지 않았습니다.
const [updateText, setUpdateText] = useState('');
const [updateImage, setUpdateImage] = useState([]);

const getPost = async (updateText, updateImage) => {
    try {
      const token = localStorage.getItem('accessToken');
      const res = await axios.get(`https://mandarin.api.weniv.co.kr/post/${postId}`, {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-type': 'application/json',
        },
      });
      updateText = res.data.post.content;
      updateImage = res.data.post.image;
    } catch (error) {
      console.log(error);
      if (error.response.data.message === '존재하지 않는 게시글입니다.') {
        alert('존재하지 않는 게시글입니다.');
      }
    }
  };
  • 해결 방법
    • state 값을 직접 변경하던 방식에서 setUpdateText, setUpdateImage를 사용해 상태를 변경하도록 수정하였습니다.
const [updateText, setUpdateText] = useState('');
const [updateImage, setUpdateImage] = useState([]);

const getPost = async () => {
    try {
      const token = localStorage.getItem('accessToken');
      const res = await axios.get(`https://mandarin.api.weniv.co.kr/post/${postId}`, {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-type': 'application/json',
        },
      });
      setUpdateText(res.data.post.content);
      setUpdateImage(res.data.post.image.split(','));
    } catch (error) {
      console.log(error);
      if (error.response.data.message === '존재하지 않는 게시글입니다.') {
        alert('존재하지 않는 게시글입니다.');
      }
    }
  };

8. api 불러오기 오류, 그리고 이어지는 map 함수의 오류

  • 문제 상황
    • jsx파일의 Return값 Fragment에서 비동기로 데이터 api가 불러와지지 않음
  • 해결방법
    • 경로는 제대로 불러온줄 알았는데 경로를 제대로 불러오지 못한것 부터가 시작이었습니다. useEffect로 axios를 불러오고 setPostList 함수에 useState를 담아 postList에 해당 리스트들을 출력할 계획이었습니다. 맨처음에는 setPostList(res.data)로 지정해두면 밑에 뜨는 data의 값들이 불러와지고, 이걸 이용해 useState를 집어넣으려 했습니다만 뒤에 posts까지 잡아 넣어줘야 바로 경로를 타고 들어갈 수 있다는 오류를 발견할 수 있었습니다.
    • 해당 post값까지 불러와야 map으로 임의의 매개변수로 돌려지는 값에 대응할때 오류없이 경로를 지정해 줄 수 있었습니다.
    • map을 사용함에 있어서도 key값 사용함에 있어서, 모든 반복되는 값에 대응하는 것이 아닌 변경이 있는 값에다만 지정해 주는 것이라는것 또한 다시한번 깨달을 수 있었습니다.
useEffect(() => {
    (async function getPost() {
      try {
        const accessToken = localStorage.getItem('accessToken');
        const res = await axios.get(`https://mandarin.api.weniv.co.kr/post/feed`, {
          headers: {
            Authorization: `Bearer ${accessToken}`,
            'Content-type': 'application/json',
          },
        });
        setPostList(res.data.posts);
        setIsRendered(true);
      } catch (error) {
        console.log(error);
      }
    })();
  }, []);