본문 바로가기
스파르타코딩클럽/내일배움캠프

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

by heereal 2022. 12. 20.

Today I Learned

  • 코딩애플 axios 관련 강의 수강
  • TO DO LIST Axios로 구현하기

 


코딩애플 AXIOS

AJAX 요청하는 방법

import axios from 'axios'

function App(){
  return (
    <button onClick={()=>{
      axios.get('https://codingapple1.github.io/shop/data2.json').then((결과)=>{
        console.log(결과.data)
      })
      .catch(()=>{
        console.log('실패함')
      })
    }}>버튼</button>
  )
}

1. axios를 쓰려면 상단에 import하고

2. axios.get(URL) 이러면 그 URL로 GET 요청이 된다.

3. 데이터 가져온 결과는 결과.data 안에 들어있다. 그래서 위의 버튼을 누르면 서버에서 가져온 데이터가 콘솔 창에 출력된다.

4. 인터넷이 안되거나 URL이 이상해서 실패했을 때 실행할 코드는 .catch() 안에 적으면 된다.

 

 

동시에 AJAX 요청 여러 개 보내는 방법

Promise.all( [axios.get('URL1'), axios.get('URL2')] )
.then(() => {

})

이렇게 작성하면 URL1, URL2로 GET 요청을 동시에 해준다. 둘 다 완료 시 특정 코드를 실행하고 싶으면 뒤에 .then()을 붙이면 된다.

 

 

서버와는 문자 자료만 주고받을 수 있다

{
  "todos": [
    { "id": 1, "title": "리액트 정복", "content": "리액트랑 친해지기", "isDone": false },
    { "id": 2, "title": "코딩 공부하기", "content": "성실하게! 열심히!", "isDone": true }
  ]
}

object/array 형대의 자료는 주고받지 못하기 때문에 object/array 자료에 따옴표를 쳐놓는 것이다. 위의 코드를 보면 모든 값을 문자열 형태로 만들어 놓았다. 이것을 JSON이라고 하는데 JSON은 문자 취급을 받기 때문에 서버와 자유롭게 주고받을 수 있다.

 

하지만 서버로부터 데이터를 받았을 때는 axios 라이브러리가 JSON -> object/array 변환 작업을 자동으로 해주기 때문에 object/array 형태의 자료가 출력된다.

 


Axios로 TO DO LIST CRUD 구현하기

TodoCard 컴포넌트 분리하기

<Header />
<Form />
<TodoList />
<Footer />

원래 메인 페이지 컴포넌트가 이런 구조였는데 TodoList 안에 TodoCard라는 컴포넌트를 추가했다.

 

import TodoCard from "../todocard/TodoCard";
import { List } from "./styled"

function TodoList ({isDone}) {

    return (
        <List>
            {/* 제목 변경-isDone이 false면 Working, true면 Done */}
            <h2>{ isDone ? "Done..! 🎉" : "Working.. 🔥"}</h2>
            <TodoCard isDone={isDone}/>
        </List>
    );
};

export default TodoList;

이렇게 Done과 Working 텍스트 밑에 TodoCard 컴포넌트가 들어간다.

 

수정 후

컴포넌트를 분리한 이유는 thunk 함수를 이용하면서 pending 과정에는 '로딩 중...'이라는 메시지가 뜨도록 설정해놨는데 Done과 Working 텍스트는 고정해 놓고 카드 부분에만 로딩 중이라는 메시지가 뜨는 게 나을 거 같다고 생각했기 때문이다.

 

 

추가하기 기능 구현하기

export const __postTodos = createAsyncThunk(
    "todos/postTodos",
    async (payload, thunkAPI) => {
        try {
            // payload는 Form.jsx에 [추가하기] 버튼 클릭했을 때 
            // __postTodos(payload)에 담아 보낸 것-새로운 Todo
            const data = await axios.post("http://localhost:3001/todos", payload)
            return thunkAPI.fulfillWithValue(payload);
        } catch (error) {
            return thunkAPI.rejectWithValue(error);
        }
    }
)

export const todosSlice = createSlice({
    name: "todos",
    initialState, 
    reducers: {},
    extraReducers: {
        [__postTodos.pending]: (state) => {
            state.isLoading = true;
        },
        [__postTodos.fulfilled]: (state, action) => {
            state.isLoading = false;
            state.todos = [...state.todos, action.payload];
        },
        [__postTodos.rejected]: (state, action) => {
            state.isLoading = false;
            state.action = action.payload;
        }
    },
})

payload로 받은 새로운 투두 리스트 객체를 axios.post를 이용해서 서버에 추가한다. 근데 새로고침을 해야만 새로 작성한 카드가 화면에 렌더링 되는데 그 이유는 axios.post로 서버에 데이터를 추가했더라도 전역 state는 변경되지 않았기 때문에 리렌더링이 되지 않는다. 새로고침을 하면 __getTodos가 실행되어 axios.get오르 서버의 데이터를 다시 불러오므로 추가한 데이터가 잘 보인다. 새로고침 없이도 리렌더링을 하고싶다면 state.todos를 변경하는 코드를 작성해야 한다.

 

 

 

useEffect 의존성 배열 수정해 보기

state.todos가 변경될 때마다 리렌더링을 해주고 싶어서 useEffect의 의존성 배열에 todos를 넣어 봤더니 'dispatch'를 빼먹었다고 메시지가 뜨더니 웹페이지에서는 무한루프가 시작되었다.

 

스크롤이 끝나지 않는 무한루프

추가나 삭제 기능을 이용한 것도 아니까 그냥 useEffect로 데이터를 불러오기만 했을 뿐인데 왜 todos를 넣었더니 문제가 발생한 건지 궁금하다.

 

const { todos } = useSelector((state) => state.todos)

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

튜터님께 질문하고 이유를 찾았다! __getTodos 함수를 dispatch하면 state.todos가 변경되는데 의존성 배열 안에 todos가 변경되니까 리렌더링하는 과정이 계속 반복이 되어서 무한 루프가 발생한 거 같다.

 

 

custom hook useInput 수정하기

const addHandler = (e) => {
    ...생략...
    // 추가하기 버튼 클릭 후 input 창 비우기
    setTitle('')
    setContent('')
}

[추가하기] 버튼을 클릭했을 때 setTitle과 setValue를 이용해서 input창을 비워주는 기능을 수행했었는데 수업에서 배웠던 useInput을 사용했더니 setState 기능을 사용할 수 없었다. 

 

const useInput = () => {
    // value는 useState로 관리하고
    const [value, setValue] = useState("");

    // 핸들러 로직도 구현한다.
    const handler = (e) => {
        setValue(e.target.value);
    };

    // 이 훅은 [ ] 을 반환하는데 
    // 첫번째는 value, 두번째는 핸들러를 반환한다.
    return [value, setValue, handler];
}

그래서 useInput return 값에 setValue를 추가해 주었다. 

 

const [title, setTitle, onChangeTitleHandler] = useInput();
const [content, setContent, onChangeContetnHandler] = useInput();

그리고 useInput을 사용할 컴포넌트에서 이렇게 작성하면 setTitle과 setContent를 사용할 수 있다.

 

삭제하기 기능 구현하기

[__deleteTodos.fulfilled]: (state, action) => {
    state.isLoading = false;
    // 수정 전
    state.todos.filter((list) => (list.id !== action.payload))
    // 수정 후
    state.todos = state.todos.filter((list) => (list.id !== action.payload))
},

처음에 이렇게 코드를 작성했는데 새로고침을 해야만 삭제된 투두 리스트가 사라지는 걸 확인할 수 있었다. 문제가 뭘까 생각하다가 filter한 것을 state.todos에 다시 할당해줘야 한다는 것을 깨달았다!

 

josn-server id 자동 생성 기능

데이터를 추가할 때 id가 자동으로 생성되는 이유가 뭔지 궁금했었는데 json-server에 id 자동 생성 기능이 있다고 한다.

"todos": [
    {
      "id": 1,
      "title": "리액트 정복",
    }
]

만약에 초기값에 id를 숫자로 작성하면 1, 2, 3 순서대로 생성되고

 

"todos": [
    {
      "id": "1",
      "title": "리액트 정복",
    }
]

1을 문자열로 작성하면 "id": "mmEWX6Y" 이런 식으로 랜덤한 문자열 id가 생성된다.

 

참고 https://stackoverflow.com/questions/53086951/json-server-strange-autoincrement-id

 

 

콘솔에 동일한 값이 여러 번 찍히는 이유 찾기

const { isLoading, error, todos } = useSelector((state) => state.todos)
useEffect(() => {
    dispatch(__getTodos());
    }, [dispatch]
);
console.log(todos)

TodoList에서 todos를 콘솔로 찍어봤는데 동일한 배열이 4번씩 반복된다. 일단 빈 배열은 __getTodos를 실행하는 과정 중 pending일 때 찍힌 것이고 밑에 4개는 데이터를 불러오고 나서 찍힌 것이다.

 

어디가 문제인 걸까 devtools까지 확인하다가 여기서도 pending과 fulfilled가 두 번씩 찍힌다는 것을 발견했다. 근데 [추가하기] 버튼을 클릭해서 postTodos를 실행했을 때는 또 한 번만 찍힌다. 아무래도 getTodos에 문제가 있는 거 같다.

 

devtools를 자세히 살펴봤더니 같은 pending이라도 requestId가 다르다.  위에서부터 1, 2, 3, 4 번호를 매겨보자면 1번과 3번이 requestId가 동일하고 2번과 4번의 requestId가 동일하다. 그래서 일단 requestId를 검색해 보기로 했다.

  • requestId: a unique string ID value that was automatically generated to identify this request sequence

 

// Thunk 함수
export const __getTodos = createAsyncThunk(
    // 첫 번째 인자: action value
    "todos/getTodos",
    // 두 번째 인자: 콜백함수
    async (payload, thunkAPI) => {
        try {
            const data = await axios.get("http://localhost:3001/todos");
            return thunkAPI.fulfillWithValue(data.data);
        } catch (error) {
            return thunkAPI.rejectWithValue(error);
        }
    }
);

일단 저 requestId는 thunk 함수에서 생성되는 거 같다.

 

thunkAPI. fulfillWithValue( data.data )가 보낸 액션 객체

여기 보면 meta에 requestId가 있다.

 

일단 여기까지 탐구한 뒤에 튜터님께 질문드렸는데 문제는 다른 곳에 있었다!

 

const Home = () => {

  return (
    <>
      <Form />
      <TodoList isDone={false} />
      <TodoList isDone={true} />
    </>
  );
};

Home 컴포넌트에 isDone 값에 따라 TodoList를 다르게 보여주기 위해 TodoList 컴포넌트를 두 개 넣어 놨었는데 이 부분 때문에 콘솔이 네 번 찍히는 것이었다. 

 

<Header>
<Form>
<TodoList>
<TodoList>
<Footer>

html 구조를 간단하게 작성하면 이렇게 되는데 내가 콘솔을 찍은 위치는 TodoList 안에 있는 TodoCard다. 그러면 기본적으로 useEffect가 두 번 실행되고, useEffect가 한 번 실행될 때마다 TodoList가 두 개이기 때문에 콘솔은 두 번씩 찍히게 되는 것이다. 2x2=4 최종적으로 콘솔은 4번 찍힌다.

 

일단 __getTodos가 중복 실행되는 것을 막기 위해 TodoList 컴포넌트 안에 있던 useEffect를 상위 컴포넌트인 Home 컴포넌트로 옮겼다. Home 페이지에 들어가면 useEffect로 서버의 데이터가 전역 state에 저장되기 때문에 하위 컴포넌트에서도 useEffect를 사용하지 않고 useSelector만으로 전역 state를 불러올 수 있게 된다.

 

여기까지 코드를 수정하면 fulfilled로 찍힌 state.todos는 2개로 줄어든다. TodoList 컴포넌트를 두 개를 넣어 놨기 때문에 하나로 줄일 수는 없다. 여전히 빈 배열이 4개가 찍히는 이유는 뭘까 궁금하긴 하지만 오늘은 그만 알아보기로 한다. 😵

 

+) 빈 배열이 4번 찍히는 이유 나의 추측.. 이해를 위해 TodoList를 하나 주석 처리하고 콘솔을 확인한다.

 

// Initial State
const initialState = {
    todos: [],
    isLoading: false,
    error: null,
};

todoSlice가 실행되는 과정에서 맨 처음에 모듈 파일에 있는 initialState의 todos를 가져온다. initialState의 todos는 빈 배열로 설정해 놨다. 이로 인해 첫 번째 줄에 빈 배열이 찍힌다. 

 

그리고 useEffect로 인해 __getTodos가 실행되면서 __getTodos.pending 과정에서 여전히 비어 있는 배열이 세 번째 줄에 찍힌다. __getTodos.fulfilled가 완료되면 비로소 API 서버에서 가져온 데이터를 전역 state에 담고, 그로 인해 다섯 번째 줄의 배열에는 투두리스트 객체가 담기게 되는 것이다.

 


Programmers 문제 풀기

머쓱이보다 키가 큰 사람

function solution(array, height) {
    let array2 = [];
    for (const person of array) {
        if (person > height) {
            array2.push(person)
        }
    }
    return array2.length
}

처음에 풀었던 방법. 습관처럼 for문을 제일 먼저 생각했지만 문득 filter를 써도 되겠다는 생각이 들었다!

 

function solution(array, height) {
    return array.filter((person) => person > height).length
}

그래서 filter()를 사용해서도 풀어봤다.

 

 

배열 두 배 만들기

function solution(numbers) {
    return numbers.map((num) => num*2)
}

map의 사용법을 설명하는 듯한 문제였다 ㅋㅋ

 

function solution(numbers) {
    return numbers.reduce((a, b) => [...a, b * 2], []);
}

다른 사람의 풀이를 보다가 reduce를 발견하고 뭔지 검색해 보기로 했다.

 

기본 구조

const sumWithInitial = array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue, initialValue
);

accumulator

누산기는 콜백의 반환 값을 누적한다. 콜백의 이전 반환값 또는 콜백의 첫 번째 호출이면서 initialValue를 제공한 경우에는 initialValue의 값이다.

 

currentValue

처리할 현재 요소.

 

initialValue (Optional)

callback의 최초 호출에서 첫 번째 인수에 제공하는 값. 초기값을 제공하지 않으면 배열의 첫 번째 요소를 사용한다. 빈 배열에서 초기값 없이 reduce()를 호출하면 오류가 발생한다.

 

참고: initialValue를 제공하지 않으면 reduce()는 인덱스 1부터 시작해 콜백 함수를 실행하고 첫 번째 인덱스는 건너뛴다. initialValue를 제공하면 인덱스 0에서 시작한다.

 

 

reduce 사용 예시

const arr = [1, 2, 3, 4, 5];
const result = arr.reduce((acc, cur, idx) => { return acc += cur; }, 0);
console.log(result);  // 15

const arr2 = [1, 2, 3, 4, 5];
const result2 = arr2.reduce((acc, cur, idx) => { return acc += cur; }, 10);
console.log(result2);  // 25

위의 예제에서 initialValue 값을 0으로 두었기 때문에 acc의 초기값은 0이 되고, 배열의 첫 번째 요소부터 acc에 자신의 값인 cur을 더해간다. reduce()를 실행하고 난 뒤, 최종적으로 반환되는 값은 0 + 1 + 2 + 3 + 4 + 5 인 15이다.

만약 initialValue 값을 10으로 둔다면, acc의 초기값은 10이 되고, 배열의 첫 번째 요소부터 acc에 자신의 값인 cur을 더해가므로 최종적으로 반환되는 값은 10 + 1 + 2 + 3 + 4 + 5 인 25가 된다.

 

const saids = [ 'ko', '안녕', 'en', 'hello', 'ja', 'こんにちは' ];

saids.reduce((acc, val, index, orig) => 
    (index === 0 || index%2 === 0) ? 
        acc[val] = orig[index + 1] : acc, acc
}, {});

대상 배열의 타입을 오브젝트나 다른 타입으로 바꿀 수 있는 것도 reduce의 장점이다. find, filter, some 등의 메서드도 배열에서 다른 값을 반환하지만 반환 형식이 고정되어 있다. 그에 반해 reduce 는 자신이 선택한 타입을 반환하게 할 수 있다. 보통은Object to Object, Array to Object 등으로 형식 변환 시 주로 사용한다. 상단의 코드는 문자열 배열을 짝수 페어끼리 키와 값으로 매핑하는 예제이다.

 

출처

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

https://miiingo.tistory.com/365

https://steemit.com/javascript/@rouka/reduce 

 

 

function solution(numbers) {
    return numbers.reduce((a, b) => [...a, b * 2], []);
}

 

[]가 초기값의 위치에 있긴 하지만 배열 형태로 값을 반환하겠다는 의미로 생각하면 된다.  a는 콜백의 이전 반환 값, b는 배열의 값 하나하나(numbers 배열을 map을 돌린다고 생각하면 된다.)를 의미한다. b에 numbers의 0번째 값 1이 담기면 1*2=2를 반환하고 이 값이 빈 배열에 담긴다. 그럼 a=[2]가 되는 것인데 이것을 spread를 사용해서 그냥 숫자 2로 만드는 것이다. 이 과정을 반복하면 numbers 배열의 각 원소를 2배로 만든 배열을 return한다.

 

일단 내가 이해한 게 맞든 틀리든 여기까지만 이해하고 오늘 reduce를 공부했다는 것에 의의를 둔다.

 


회고

오전에 갑자기 프로그래머스 풀다가 reduce 메서드를 이해하기 위해 시간을 너무 많이 쓴 거 같다 ㅎㅎ 오늘은 axios로 투두 리스트 추가, 삭제 기능을 구현했다. 그리고 콘솔이 네 번 찍히는 이유를 찾기 위해 엄청나게 고생했다. 그래서 그런지 유독 지친 기분이다. 빨리 씻고 자고 싶다~~🛌🏼

 

댓글