SurveyProject_admin) 클론코딩 정리하기

iamokian·2022년 8월 2일
0

survey프론트단을 클론 코딩하고 바로 뒤이어 시작한 어드민페이지 클론코딩. 만들어낸 설문페이지의 각 설문들을 관리하는 영역이다. 이것도 프론트와 마찬가지로 흐름을 이해하고, 라이브러리들을 사용하는 이유를 살펴보기에 좋은 프로젝트였다. 더불어 연계되는 개념도 찾아보며 이해되는과정이 되기도 했다. 하지만 기능구현은 역시나 쉽지않았으므로 조금 더 코어를 탄탄하게 해줄 클론코딩을 해봐야겠다, 자바스크립트 딥다이브 2회독도 빨리 마무리해야지. n회독이 목표:)
무튼, 이번 클론코딩한것을 단계별로 일지처럼 적어본다.

  1. 프론트단과 마찬가지로 기획서에서 요구하는 기술과 필요한 구조를 설계해야한다.
    참고로 이 영역에서는 디자인 시안이 없다. 모든 프로젝트가 그런것은 아니지만 보통의 어드민은 디자인시안 없이 기획서나 와이어프레임을 토대로 지정된 템플릿을 사용해 페이지를 그려낸다.

기본적인 세팅을 치루고 기획서를 살펴보며 어떤 영역으로 나눠야 하는지 알아보았다. 우선 크게 페이지를 설문조사관리를 하는 메인으로 보여질 리스트페이지와, 게시물을 누르면 게시글을 수정할 수 있는 빌더페이지 총 2개 페이지로 나눴다. 그리고 그안에서 추가적으로 컴포넌트를 쪼개 폴더별로 관리 했다.

📦pages
 ┣ 📜BuilderPage.js
 ┗ 📜ListPage.js
📦components
 ┣ 📂AddButton
 ┃ ┗ 📜index.js
 ┣ 📂Body
 ┃ ┗ 📜index.js
 ┣ 📂BuilderTitleInput
 ┃ ┗ 📜index.js
 ┣ 📂Card
 ┃ ┗ 📜index.js
 ┣ 📂FloatingButton
 ┃ ┗ 📜index.js
 ┣ 📂OptionSection
 ┃ ┗ 📜index.js
 ┣ 📂PreviewSection
 ┃ ┗ 📜index.js
 ┣ 📂SelectInput
 ┃ ┗ 📜index.js
 ┣ 📂TextAreaInput
 ┃ ┗ 📜index.js
 ┗ 📂TextInput
 ┃ ┗ 📜index.js
  1. 그리고 어드민 페이지에서는 레이아웃을 구현하고 컴포넌트를 만들어낼 때 AntDesign을 사용했다. UI라이브러리를 사용함으로 인해 다양한 ux컴포넌트와 속성을 사용해 쉽게 형태를 변환할 수 있었다. 그리고 antd말고도 다양한 라이브러리가 있는데, 그것을 선택함에 있어서는 기준이 있어야했다. 해당프로젝트에서는 antd가 적합하므로 그리 진행을 했다.

UI라이브러리를 선택하는 기준을 생각해보자

  • 내가 원하는 컴포넌트가 있는지
  • 사용하기는 편리한지
  • 디자인이 서비스와 어울리는지
  • 스타일의 커스텀이 가능한지
  • 문서화가 잘 되어 있는지
  • 성능에 문제가 없는지

antd 공식문서에 들어가 기본 사용법 체크 후 진행. 설치 후 index페이지에 임포트시켰다.

import 'antd/dist/antd.min.css';

단순 컴포넌트뿐만 아니라 레이아웃도 잡을 수 있다.

import { Layout, Menu } from 'antd';

const { Header, Content, Sider } = Layout;

function MainLayout({ selectedKeys, children, padding = 45 }) {
  const contentStyle = useMemo(() => {
    return { padding };
  }, [padding]);

  return (
    <Layout
      style={{
        minHeight: '100vh',
      }}
    >
      <Sider>
        ...
      </Sider>
    </Layout>
  );
}

export default MainLayout;

input, button역시 일반 태그가 아닌 antd 컴포넌트를 사용했다.

import { Input } from 'antd';

const { TextArea } = Input;

function TextAreaInut({ options }) {
  return <TextArea placeholder={options.placeholder} maxLength={options.max} />;
}

export default TextAreaInut;
  1. 어드민에서의 페이지도 react-router-dom을 사용했다.

브라우저 라우터로 App 먼저 감싸주고.

import { BrowserRouter } from 'react-router-dom';

import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>,
);

라우트 등록하기. 이때 페이지 주소에 따라 다른 페이지 컴포넌트를 렌더링할 수 있도록 설계하였다. 각 빌더페이지는 설문데이터의 id값으로 분류해 페이지를 달리한다.

import { Route, Routes } from 'react-router-dom';

import BuilderPage from './pages/BuilderPage';
import ListPage from './pages/ListPage';

function App() {
  return (
    <div className="App">
      <Routes>
        // 루트경로에서도 list가 보일수 있도록 설정하였다.
        <Route path="/" element={<ListPage />} />
        <Route path="/list" element={<ListPage />} />
        <Route path="/builder" element={<BuilderPage />} />
        <Route path="/builder/:surveyId" element={<BuilderPage />} />
      </Routes>
    </div>
  );
}

export default App;


추가적으로 설문 조사 관리 메뉴를 active, none-active를 시키는 방법을 따라해보며 antd를 사용한 페이지간의 라우터연동을 간단하게 알 수 있었다.

...
// 하위 컴포넌트로부터 selectedKeys 값을 받아와 메뉴의 defaultSelectedKeys에 값을 넣어주는 것이다.
function MainLayout({ selectedKeys, children, padding = 45 }) {
  ...

  return (
    <Layout
      style={{
        minHeight: '100vh',
      }}
    >
      <Sider>
        ...
        <Menu theme="dark" defaultSelectedKeys={selectedKeys} mode="inline">
          <Menu.Item key="list">
            <Link to="/list">설문 조사 관리</Link>
          </Menu.Item>
        </Menu>
      </Sider>
      ...
    </Layout>
  );
}

export default MainLayout;
function BuilderPage() {
  ...

  return (
    // 상위 컴포넌트로 전달할 props에 등록한 Menu의 키값과 props 값 일치시켜서 전달해주기.
    <MainLayout selectedKeys={['list']} padding={0}>
      ...
      <FloatingButton />
    </MainLayout>
  );
}

export default BuilderPage;

4.어드민 페이지에서는 API데이터를 가져오기위해 React Hooks인 useSWR을 사용했다. useSWR은 말그대로 API데이터를 가져오기위한 훅으로, 프론트페이지에서 recoil로 API데이터를 반환하는 훅을 만들었었는데, 무슨 차이가 있는가 하니 그전에 API데이터를 어떻게 효율적으로 사용할수 있을까에 대한 고민이 선행되어야 했다.

리스트페이지에서 useSWR을 선택한 이유
페이지가 로드될때마다 서버에서 데이터를 가져오는 것은 매우 비효율적이다. 그리하여 처음 요청 이후 두번째 로드시에는 캐시를 사용해 저장된 캐시로 데이터를 보여주는데. 이때 만약 서버에 있는 데이터가 업데이트가 되었다면 기존 캐시로 보여진 데이터와 내용이 달라 곤란하다. 그럴때에는 우선 캐시에 저장된 데이터를 보여주고 그 다음 서버에서 업데이트 된 데이터를 보여지게 하는것.

그러므로 앞으로도 다양한 API데이터를 가져오는 훅스나 라이브러리를 사용할 때 이런식의 고려와 공부가 필요하다는것을 알 수 있었다.

axios를 사용해 fetcher함수를 만들어두고,

import axios from 'axios';

axios.defaults.baseURL = 'http://localhost:3001';

// 공식문서 내용 참고하여 그대로 가져옴
const fetcher = (url) => axios.get(url).then((res) => res.data);

export default fetcher;
function ListPage() {
  const { data, error } = useSWR(
    '/surveys?_sort=id&_order=desc',
    fetcher,
  );
  ...
}

인자는 아래와 같이 쓰인다.

  • '/surveys?_sort=id&_order=desc' : 내가 호출하고자 하는 API경로
  • fetcher : API를 불러올 로직(axios와 같은 라이브러리 사용이 가능, 찾아보니 useSWR은 데이터를 가져오는것에 특화되어있다는 말이 있다. 적재적소에 사용하기 위한 공부가 필요)

그렇게 데이터를 불러와 콘솔창에서 확인을 해보면

한번의 undefined와 받아온 데이터들 총 2번의 로그를 확인할 수 있다. 이것이 위에 적어둔 데이터를 받아오기까지 걸리는 시간동안 기존의 캐시데이터가 있다면 그것을 보여주고 그게아니라면 undefined를 내보낸것. 그리고 서버에서 데이터를 다 받아오면 data를 그려준다. 이것이 useSWR의 방식이다. 제대로 개념을 알지못하고 지나갔다면 아쉬울뻔.

~추가로 편리한듯 복잡한듯 편리한 antd테이블 사용(유용해보여서 상기시키기)~

import { Button, Table } from 'antd';
...

function ListPage() {
  ...

  const columns = useMemo(
    () => [
      {
        title: '번호',
        dataIndex: 'id',
        key: 'id',
      },
      {
        title: '제목',
        dataIndex: 'title',
        key: 'title',
      },
      {
        title: '생성일',
        dataIndex: 'createdAt',
        key: 'createdAt',
        render: (createdAt) => {
          const time = new Date(createdAt);
          return `${time.getFullYear()}-${
            time.getMonth() + 1
          }-${time.getDate()}`;
        },
      },
      {
        title: '액션',
        dataIndex: 'id',
        key: 'action',
        render: (id) => {
          return (
            <Button
              danger
              onClick={(e) => {
                deleteSurvey(id).then(() => mutate());
                e.stopPropagation();
                e.preventDefault();
              }}
            >
              삭제
            </Button>
          );
        },
      },
    ],
    [mutate],
  );
  if (error) {
    return 'error';
  }

  if (!data) {
    return 'loading...';
  }

  return (
    <MainLayout selectedKeys={['list']}>
      <CreateButtonWrapper>
        <Button onClick={() => navigate('/builder')}>
          새로운 설문조사 생성
        </Button>
      </CreateButtonWrapper>
      <Table
        onRow={(record, rowIndex) => {
          return {
            onClick: (event) => {
              // 해당 리스트를 클릭하면 record가 가져오는 obj값안에서 id값을 가져와 해당설문페이지로 이동
              navigate(`/builder/${record.id}`);
            },
          };
        }}
        pagination={{
          total: data.length,
          current: page,
          pageSize: PAGE_SIZE,
        }}
        onChange={(pagination) => {
          setPage(pagination.current);
        }}
        columns={columns}
        dataSource={data.map((item) => ({ ...item, key: item.id }))}
      />
    </MainLayout>
  );
}

const CreateButtonWrapper = styled.div`
  text-align: right;
  margin-bottom: 25px;
`;

export default ListPage;

columns에 컬럼속 각각의 카테고리와 값들을 담아두고 Table에 속성을 채워 뿌려준다. 카테고리속 render는 일반 string값이라면 그대로 화면에 보여주지만, 가공할 데이터가 있으면 그것을 가공하여 랜더링 시켜주는것이다.
데이터는 dataSource속성에 아까 useSWR을 사용해 가져온 데이터를 넣어주었다.

그리고 설문 수정페이지와 질문추가/삭제 컴포넌트를 구현하며 antd에서 grid 레이아웃을 활용해 설문리스트 / 문항옵션과 같이 영역을 나누었다.

import { Col, Row } from 'antd';
...

function BuilderPage() {
  ...

  return (
    <MainLayout selectedKeys={['builder']} padding={0}>
      <Row style={{ height: '100%' }}>
        // 좌측 영역 너비값
        <Col flex="auto" style={{ padding: 30 }}>
          <BuilderTitleInput />
          <PreviewSection />
        </Col>
        // 우측 영역 너비값(고정값으로 설정)
        <Col flex="350px">
          <OptionSection />
        </Col>
      </Row>
      <FloatingButton />
    </MainLayout>
  );
}

export default BuilderPage;
  1. 최상위 컴포넌트인 빌더페이지에 임시데이터를 넣어두니 propDrilling이 발생한다. 그러므로 이를 해결하기 위해 Redux를 사용해 빌더 페이지의 상태를 빼내었다. 프론트페이지에서는 Recoil을 어드민 페이지에서는 Redux를 사용해 두가지 모두 사용해보게 된것이다.

Redux를 접하며 알게된 기본내용

  • store는 전역상태 저장소이다.
  • 컴포넌트는 store에서 필요한 state를 구독하여 사용한다.
  • 상태가 변경되어야 할 시 action객체를 만든다.
  • action을 dispatch하여 reducer에게 넘겨준다.
  • reducer는 액션의 정보를 보고 state를 정해진 규칙에 따라 변경해준다.

최종적으로 globalState가 변경이 되면 해당 state를 구독하고 있던 컴포넌트는 새로운 state값을 받고 해당 컴포넌트가 리렌더링 되는것이다.

하지만 이 프로젝트에서는 순수 리덕스의 복잡한 코드를 간소하여 사용하고자 ReduxToolkit를 사용했다.

설치할 때에 리덕스 툴킷만 설치를 해도 리덕스코어가 설치되지만 react-redux도 같이 설치해야 리액트와 리덕스를 연결시켜준다.

npm i @reduxjs/toolkit react-redux
...
import { Provider } from 'react-redux';

import store from './stores';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>,
);

전역에서 사용하기 위해 App.js에 Provider로 감싸주자. 이제 하위 컴포넌트들은 store에 접근할 수 있다.

import { configureStore } from '@reduxjs/toolkit';

import thunk from './middleware/thunk';
import selectedQuestionIdReducer from './selectedQuestionId/selectedQuestionIdSlice';
import surveyReducer from './survey/surveySlice';

export default configureStore({
  reducer: {
    survey: surveyReducer,
    selectedQuestionId: selectedQuestionIdReducer,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
});

각각의 리듀서를 만들어두고 데이터를 가져오거나 수정이 필요할때 사용하기.

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  data: null,
  loading: false,
  error: null,
};

export const surveySlice = createSlice({
  name: 'survey',
  initialState,
  reducers: {
    setTitle: (state, action) => {
      // _.payload가 새로 변경된 값을 가져온다.
      state.data.title = action.payload;
    },
    ...
  },
});

export const {
  ...
} = surveySlice.actions;

export default surveySlice.reducer;

createSlice는 createReducer과 creatrAction을 합친것이다. Redux코어에서 리듀서와 액션크리에이터를 각각 만들어줘야 한다면 툴킷은 리듀서에 대한 로직을 작성하면 그 이름을 바탕으로 자동으로 액션크리에이터를 만들어준다. 그리고 createSlice는 Immerlib를 사용하고 있어서 불변성유지도 알아서 척척 해준다

...
import { useDispatch, useSelector } from 'react-redux';

import { setTitle } from '../../stores/survey/surveySlice';

function BuilderTitleInput() {
  const dispatch = useDispatch();
  const title = useSelector((state) => state.survey.data?.title || '');
  return (
    <Input
      placeholder="설문 제목을 입력해주세요."
      value={title}
      onChange={(e) => {
        dispatch(setTitle(e.target.value));
      }}
    />
  );
}

export default BuilderTitleInput;
  • useSelector: store에서 데이터를 뽑아오기위해 사용, 어떤 상태값을 가져오기위해 함수 정의
  • useDispatch: action을 dispatch하기 위해 사용

그리고 디스패치된 액션들을 볼 수 있는 도구 리덕스 데브툴즈를 활용해 값이 변경되는것을 일일히 체크할 수 있었다.
쌓이는 각각의 기록들을 히스토리창에서 전부 돌려볼수있어서 에러가 난다면 어느지점에서 나는지도 확인이 가능했다.

  • action: action create에 의해 생성된 객체를 보여준다
  • diff: 이전값과 변경된값을 보여준다

6.설문을 수정하고 추가할 수 있는 빌더페이지에서는 Redux에서 API를 연동해 데이터를 가져왔다. 하지만 이 프로젝트에서는 라이브러리를 사용하지 않고 직접 Redux에서 API를 호출했다. 강의에서의 요지는 리덕스에서 비동기처리하는 방식을 구현해 알아보고자 함이었다.

비동기코드를 리덕스에서 처리하는 기본개념
리듀서를 사용할때에는 순수함수가 사용되야 하므로 API를 요청하는 비동기방식의 처리를 요구할 수 없다(값이 순수하지못하고 불분명한). 그리하여 해당 스텝에서 배운것이 middleware와 그 개념이다. 만약 컴포넌트에서 비동기처리를 요구하는 action을 reducer로 보낸다면 그것을 middleware가 가로채 then.()이후의 코드에 새로운 set어쩌구를 반환해 스토어에 넣어준다. 만약 error를 일으킨다면 setError을 반환할 것이다.

...

import thunk from './middleware/thunk';
...

export default configureStore({
  reducer: {
    ...
  },
  // 미들웨어영역에 미들웨어를 넣고 함수의 인자로 기존의 미들웨어를 넣고 새로운 미들웨어를 붙여주는 것이다.
  // thunk는 아래 추가정리
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
});

그리고 미들웨어 함수안에서 API를 보내야할 때 직접 호출하지 않고 thunk 패턴을 사용. 이것은 일종의 리덕스의 패턴이라고 한다. 이는 action을 전달 받을 때 action에 로직을 다 넣어두고 그 액션을 미들웨어에 던진다. 그렇게 되면 thunk미들웨어 안에서 그 로직을 받아서 미들웨어 내부에서 실행을 해준다. 그럴 때 API를 호출하는 로직이 있다면 그 호출시점은 thunk안에서 호출이 되는것이다. 주저리주저리 적었는데.. 간추리자면,
->로직을 담은 액션을 받아서 실행해주는 함수 = thunk함수

const thunk = (store) => (next) => (action) => {
  // 액션의 타입이 함수라면 아래구문 실행
  if (typeof action === 'function') {
    // 로직이 담긴 함수를 실행할 때 store의 추가정보가 필요할수 있으니 getState, dispatch함수를 넣어두기.
    // 그렇게 해야 API가 넘어왔을때 이 안에서 함수를 실행 후 setData액션으로 스토어에 저장할 수 있다.
    action(store.dispatch, store.getState);
  } else {
    // 그것이 아니라면 바로 넘겨주기
    next(action);
  }
};

export default thunk;

API를 요구하는 thunk 코드를 따로 분리하여 관리했다. 관심사 분류를 위해 이또한 services 폴더를 만들어 따로관리!

import fetcher from '../lib/fetcher';
import { setError, setLoading, setSurvey } from '../stores/survey/surveySlice';

const fetchSurvey = (surveyId) => (dispatch, getState) => {
  setLoading(true);
  fetcher(`/surveys/${surveyId}`)
  	// 정상 실행
    .then((data) => {
      dispatch(setSurvey(data));
    })
  	// 에러 발생시
    .catch((err) => {
      dispatch(setError(err));
    })
  	// 최종 실행(로딩상태 구분)
    .finally(() => {
      setLoading(false);
    });
};

export default fetchSurvey;
  1. 진행을 하며 프로젝트의 렌더링이 비효율적으로 진행되고 있음을 인지시켜주었다. 파악을 위해 사용된 react-developer-tools. 컴포넌트에 입력을 한다거나, 값을 넣고 바꾸는 과정에서 렌더링체크를 바로바로할 수 있었다.

사이즈가 매우 부담스러운데.. 무튼 타이틀 값만 변경하려하는데 저 온갖 컴포넌트들이 재렌더링되는 엄청난 확인을 했다. 실제 서비스되는 제품이라면 정말 최악이겠지. 리액트를 사용하는 의미가 단하나도없는:)..
그리하여 다른 컴포넌트들이 계속해서 렌더링 되는 이유부터 파악을 했다.
현재는 최종코드로 코드들이 다 정리가 되었지만, 진행되는 과정에서는 데이터가 변경되는 컴포넌트도 전부 메인 빌더페이지에 놓여져있었다. 물론 서베이 데이터가 변경되면(현재 서베이데이터를 빌더페이지전체가 구독중.) 내부 내용들이 변경되는게 맞지만 텍스트만 변경해도 전부 다시 실행되는것은 문제였다.
-> 해결책으로는 globalState를 구독하고 있는 컴포넌트들을 최대한 밑으로 내려줘야 한다는것을 배웠다. 그래서 위 이미지속 랜더링을 발생시키는 타이틀변경하는 input영역을 컴포넌트로 따로 분류해 수정해주었다. 그리고 추가적으로 빌더페이지 전체에서는 필요로 하지않는 서베이데이터를 무조건 필요로하는 섹션으로 내려주었다.

그리고 깔끔해진 렌더링되는 모습 :)

8.수정한 설문들을 저장하는 기능을 구현하며 put과 post의 차이도 알 수 있었다. http메소드의 종류는 여러개가 있는데 그 중 가장 흔하게 사용되는

  • GET: 일반적으로 데이터를 가져올 때
  • POST: 새로운 데이터를 저장할 때
  • PUT: 데이터를 저장하되 기존의 데이터를 수정면서 저장할 때
  • DELETE: 삭제할 때

이를 바탕으로 PUT메소드를 활용해 이미 저장되어있는 설문데이터를 수정 후 저장하는부분을 구현했다. 각각의 API를 요청하는 함수 모음 폴더링.

📦services
 ┣ 📜deleteSurvey.js
 ┣ 📜fetchSurvey.js
 ┣ 📜postSurvey.js
 ┗ 📜putSurvey.js
import axios from 'axios';

function putSurvey(survey) {
  axios
    .put(`/surveys/${survey.id}`, survey)
    .then(() => alert('저장되었습니다.'));
}

export default putSurvey;

해당 함수를 저장하기 버튼에 달아주었다.

import postSurvey from '../../services/postSurvey';
import putSurvey from '../../services/putSurvey';

function FloatingButton() {
  const navigate = useNavigate();
  const survey = useSelector((state) => state.survey.data);

  if (!survey) {
    return null;
  }

  const isEditPage = !!survey.id;
  return (
    <FloatingButtonWrapper>
      <Button
        onClick={() =>
          isEditPage
            ? putSurvey(survey)
            : postSurvey(survey).then((data) => {
                navigate(`/builder/${data.id}`);
              })
        }
      >
        저장
      </Button>
    </FloatingButtonWrapper>
  );
}

하지만 저장을 하는 데이터가 신규 데이터일수도 있고, 수정된 데이터를 저장하는 것 일수도 있으니 isEditPage로 기존에 있던 페이지인지 아닌지를 체크하여 post, put으로 분개시켜주었다.

9.최종적으로 구현한 새로운 설문생성기능. 여태까지는 설문을 수정하고 저장만했다. 하지만 그러면 어드민 페이지가 아니다:) 새로운 생성도 할 수 있어야 하는법.
이미 존재하는 설문페이지와, 새로생성하는 설문페이지의 차이점과 고려할 점을 생각해보자.
1. 주소가 달라야 한다. 생성할 설문페이지에는 아직 id가 없지만 기존 설문에는 id가 있다.
2. 기존 생성된 설문은 API를 통해서 받아오지만 신규설문페이지는 아무것도 없는상태이다.
3. 신규설문 페이지에는 초기데이터가 필요하다.(store의 초기 데이터가 null이므로 따로 초기값 세팅없이 수정폼으로 들어가게 되면, 값을 입력해도 필드를 찾아가지 못해서 에러가 발생할 수 있다.)
4. 저장버튼의 역할이 다르다. put, post의 차이 (위에 저장버튼을 설명하며 미리적음)

수정하여 저장한건지, 새로 생성해 저장한건지도 확인 완료. dbJSON에 새로운 아이디가 생성되어 잘 들어간것도 확인가능했다.

그리고 신규생성 페이지가 생성된 후 '/builder' 주소 뒤에 생성된 고유 아이디 값이 붙여주기위해 then체이닝하기.
postSurvey에서 값을 리턴시켜서 promise객체를 전달시켜줄 수 있도록 수정.

import axios from 'axios';

function postSurvey(survey) {
  return axios
    .post(`/surveys`, { ...survey, createdAt: new Date().getTime() })
    .then((res) => {
      alert('저장되었습니다.');
	  // 결과물의 data를 보내준다.
      return res.data;
    });
}

export default postSurvey;

그리고 받아온 promise객체로 then체이닝을 시켜준다. 얼럿창이 닫히면 주소가 변경될것이다.

function FloatingButton() {
  const navigate = useNavigate();
  const survey = useSelector((state) => state.survey.data);

  ...

  const isEditPage = !!survey.id;
  return (
    <FloatingButtonWrapper>
      <Button
        onClick={() =>
          isEditPage
            ? putSurvey(survey)
  			// then 체이닝으로 data를 사용해 주소끝에 id 붙여주기!
            : postSurvey(survey).then((data) => {
                navigate(`/builder/${data.id}`);
              })
        }
      >
        저장
      </Button>
    </FloatingButtonWrapper>
  );
}

인터넷강의를 보고 클론코딩을 하는것이지만, 단순히 따라만하고싶지 않았다. 왜 사용하는지 알고싶었고, 어떻게 사용하는지도 알고싶었다(어떻게는 역시 어려웠다). 그리고 당연히 실무에 비할수 없지만 비슷한 느낌으로 어떻게 흘러가는지도 체험을 해보고싶었는데 이는 그것을 알려주는 강의였다. 더불어 파생되는 개념들을 궁금하면 찾아보고, 잘 이해가 가지않았던 개념도 이해를 해볼수 있어서 좋았다. 더불어 이렇게 포스팅하면서 어려움에 현타받고 따라치기만 했던것 같은 부분을 다시 이해하고 넘어가서 더욱 좋다. 앞으로 뭘하든 일지를 남겨보자:)

profile
필기하고 기록하고

0개의 댓글