[TIL] 내일배움캠프 React 과정 2022.12.21

Today I Learned

  • TO-DO-LIST Axios로 CRUD 구현하기


TO DO LIST Axios로 CRUD 구현하기

상세 페이지에서 새로고침 하면 에러가 뜨는 문제

상세페이지에 들어간 후에 새로고침을 하면 이렇게 ID가 undefined라며 에러가 뜬다. 지금까지 대수롭지 않게 생각했었는데 뭔가 이게 큰 문제라는 생각이 들어서 해결하고 싶어졌다. 또 다른 고생길의 시작...

콘솔에 찍어봤을 때 state가 3번의 과정을 거쳐서 저장되는 거 같다. 그래서 첫 번째, 두 번째 시도에는 state에 아예 값이 없어서 실패하고 state를 저장하지 못했던 거였다. 근데 그냥 Home에서 useEffect로 __getTodos를 실행했을 때는 이런 문제가 없었는데 왜 Detail과 Edit 페이지에서만 이런 문제가 발생하는 걸까?

const todo = todos.find((list) => list.id === param.id);

문제의 원인은 find 메서드에 있었다. MDN 문서에 보면 "find() 메서드는 주어진 판별 함수를 만족하는 첫 번째 요소을 반환합니다. 그런 요소가 없다면 undefined를 반환합니다."라고 적혀있다. todos.find를 해서 특정 id를 가진 객체를 하나만 가져와야하는데 처음에는 todos가 빈 배열이기 때문에 특정 요소를 찾을 수 없고, 그래서 todo가 undefined가 뜨는 것이다.

<ID>ID: {todo?.id.slice(0, 8)}</ID>
<h2>{ todo?.isDone ? "Done..! 🎉" : "Working.. 🔥"}</h2>
<Title>{todo?.title}</Title>
<Content>{todo?.content}</Content>

일단 이 문제를 해결하기 위해서는 이렇게 optional chaining을 사용해서 참조값이 undefined인 경우가까지 대비해줘야 한다. 근데 Detail과 Edit 페이지에서 optional chaining을 사용하더라도 또 문제점이 발생한다. 😫

Detail 페이지에서는 문제가 없는데 Edit 페이지에서 새로고침을 하면 input에 불러오던 todo.title과 todo.content가 사라지는 문제가 발생한다.

Edit 페이지에서 콘솔을 찍어보면 todos를 가져오긴 하는데 그 전 단계에서 todos에 담긴 값이 없어서 undefined가 뜨는 거 같다.

const [title, setTitle] = useState(todo?.title);
const [content, setContent] = useState(todo?.content);

const Edit = () => {
    <TitleInput id="title" value={title} onChange={inputContent} />
    <ContentInput id="coneent" value={content} onChange={inputContent} />
};

Edit 페이지에 들어갔을 때 기존 title과 content를 불러오기 위해서 input의 value를 각각 title과 contetn로 설정하고 useState에 기존 데이터인 todo.title과 todo.content를 담아뒀는데 useState는 처음 딱 한 번만 실행된다고 한다. 그러니까 새로고침을 하면 state가 빈 배열인 상태에서 useState에 todo.title과 todo.content를 넣을 수 없는 것이다. 일단 optional chaining을 사용했기 때문에 에러는 발생하지 않는 거 같지만 이 문제를 해결하고 싶었다.

useEffect(() => {
    // todos에 값이 없으면 아무것도 실행 x
    if(todos.length < 1) return ;

    // 새로고침 후 todos에 데이터가 들어왔을 때 
    // input에 todo의 title과 content를 넣음
    const todo = todos.find((list) => list.id === param.id);
    setTitle(todo?.title);
    setContent(todo?.content);

    }, [todos]
);

튜터님에게 해결 방법을 배울 수 있었는데 Edit 컴포넌트에 useEffect를 하나 더 작성하고 의존성 배열에 todos를 담아서 todos의 값이 변경될 때마다 useEffect를 실행하도록 한다. 일단 todos.length > 1일 때, 즉 todos가 빈 배열일 때는 아무것도 실행하지 않고(불필요한 useEffect 실행을 막기 위해), todos에 서버의 데이터가 들어가면 todos.find로 todo 객체 하나를 할당하고, setState로 title과 content의 값을 변경한다. 그러면 input의 value인 title과 content도 변경되기 때문에 기존 데이터를 가져올 수 있는 것이다..

새로 알게 된 사실 하나.
useState와 useEffect는 조건문 아래에서는 사용을 못한다고 한다!

이걸 텍스트로 설명하려니 아무도 이해하지 못할 거 같지만 아무튼 그렇다..
오늘 하루종일 싸웠던 새로고침 문제 해결!

useEffect 사용 최소화하기

useEffect(() => {
	dispatch(__getTodos());
	}, [dispatch]
);

나는 라우트하는 페이지마다 즉 Home, Detail, Edit 페이지마다 서버의 데이터를 state에 저장하기 위해 useEffect를 실행해랴 한다고 생각했었는데 튜터님의 설명을 들어보니 첫 페이지인 Home에서만 __getTodos를 실행해서 state에 데이터를 저장하면 다른 페이지에 가도 state에 저장된 상태로 남기 때문에 Home에만 useEffect를 실행해도 충분한 것이다.

근데 Home에만 useEffect를 넣게 되면 Detail이나 Edit 페이지에서 새로고침을 했을 때 useEffect를 실행할 수 없어서 state에 서버의 데이터가 저장되지 않는 문제가 발생한다. 그래서 요청의 범위를 줄이기 위해 Detail이나 Edit 페이지에서는 해당 id를 가진 객체 하나만 __getTodosById라는 함수를 만들어서 불러오는 게 어떨까 생각을 했다.

하지만 이 방법은 실패했다. Detail 페이지에서 새로고침을 했을 때는 해당 id를 가진 데이터를 잘 불러오지만 뒤로 가기를 해서 Home 페이지로 돌아갔을 때 여기서는 useEffect가 실행되지 않아서 state에는 Detail 페이지에서 불러온 객체 하나밖에 없기 때문에 전체 데이터를 불러오는 데 문제가 발생한다. 그래서 결국 일단은 Home, Detail, Edit 각 페이지마다 서버의 전체 데이터를 state에 저장하는 useEffect를 넣어주기로 했다.

axios.get으로 항상 최신 서버 데이터를 가져오기

export const __postTodos = createAsyncThunk(
    "todos/postTodos",
    async (payload, thunkAPI) => {
        try {
            await axios.post("http://localhost:3001/todos", payload)
            return thunkAPI.fulfillWithValue(payload);
        } catch (error) {
            return thunkAPI.rejectWithValue(error);
        }
    }
);

[__postTodos.fulfilled]: (state, action) => {
    state.isLoading = false;
    state.todos = [...state.todos, action.payload];
}

원래 추가하기 버튼을 클릭했을 때 작동하는 __postTodos의 흐름을 이러했다. axios.post를 실행해서 json-server에 데이터를 추가했지만 새로고침 전까지는 이것이 state에 반영되지는 않는다. 그래서 __postTodos.fulfilled일 때 state.todos를 따로 수정해줘야 했다.

그런데 다른 분의 코드를 보고 __postTodos에서 axios.post를 하고 난 다음에 axios.get을 하는 방법도 있다는 것을 알게 됐다. 튜터님께 물어보디 두 가지 방법 중에 선택하는 건 자유지만 후자를 선택하면 state에 항상 서버의 최신 데이터가 업데이트되는 장점이 있다고 하셔서 한번 구현해보기로 했다.

export const __postTodos = createAsyncThunk(
    "todos/postTodos",
    async (payload, thunkAPI) => {
        try {
            await axios.post("http://localhost:3001/todos", payload)
            const data =  await axios.get("http://localhost:3001/todos")
            return thunkAPI.fulfillWithValue(data.data);
        } catch (error) {
            return thunkAPI.rejectWithValue(error);
        }
    }
);

[__postTodos.fulfilled]: (state, action) => {
    state.isLoading = false;
    state.todos = action.payload;
},

그래서 __postTodos 안에 axios.get 코드를 추가하고 return 부분에 인자도 data.data로 수정한 다음에 fulfilled 했을 때 __getTodos 방식과 동일하게 state.todos에 action.payload를 할당하는 방식으로 코드를 수정했더니 이래도 정상적으로 작동이 된다.


회고

오늘 눈이 왔다고 깃헙 잔디가 파란색으로 변했다~~

오늘 진짜 하루종일 새로고침 문제로 싸운 거 같다. 결국엔 튜터님들의 도움으로 해결했지만 json-server와 aixos, useEfeect 등에 대해서 많이 배울 수 있었다. 그냥 튜터님들이 하라는 대로 코드를 작성하는 게 아니라 왜 그래야 하는지, 이 코드가 어떤 구조로 작동하는지 이해하기 위해서 많이 노력했다.

그리고 오늘 선발대 OT가 있었는데 이곳은 내가 낄 자리가 아닌 거 같다. 선발대 튜터님이 생각하는 선발대 기준은 내가 며칠 동안 고생하면서 json-server로 구현한 이 웹페이지를 2시간 안에는 완성할 수 있을 정도라고 하신다. 그래서 아마 나는 선발대를 나오게 될 거 같다 ㅎㅎ 더 배울 수 있는 기회를 놓쳐서 아쉽긴 한데 그만큼 정규 과정을 정말 제대로 배우겠다는 마음가짐을 가져야겠다.