View on GitHub

FireFours

React Study: 발등에 불떨어진 4학년들의 모임

2020년 1월 12일

1. 배운 내용

13. 리액트 라우터로 SPA 개발하기

  1. SPA란 Single Page Application의 약어로 한 개의 페이지로 이루어진 애플리케이션을 의미한다. 최초로 html파일을 다운로드 받은 후, 그 이후엔 json 파일로 데이터만 업데이트하는 형식이다. 정말 하나의 화면을 가진 것은 아니고 라우팅(Routing)을 통해 글보기, 글쓰기 창 등 여러 화면으로 이동할 수 있다.
    • 여러 창을 가지는 규모가 큰 애플리케이션의 경우 자바스크립트 파일이 비대해질 수 있다는 문제가 있으나 이를 추후에 배울 코드 스플리팅을 통해 해결할 수 있다.
  2. SPA를 구현하기 위한 모듈로 react-router가 대표적으로 이를 yarn add react-router-dom으로 설치한다. 그런 후 전체 <App /><BrowserRouter>로 감싸준다.
  3. 가장 중요한 컴포넌트는 <Route path={["/경로명", "/경로명2"]} component={컴포넌트이름}/> 으로 경로와 해당 경로에 맞는 컴포넌트를 보여준다. 그리고 이와 같이 Route와 연결된 컴포넌트는 match, location, history 세가지 props을 기본적으로 받게 된다.
  4. <Link to="/경로명">링크 이름</Link>은 해당 경로로 갈 수 있도록 해주는 링크를 생성한다.
  5. <Switch>...</Switch> 컴포넌트는 여러 <Route>를 감싼 후 링크와 일치하는 단 하나의 컴포넌트만을 보여준다. 만약 설정하지 않는다면 조건에 충족하는 모든 컴포넌트가 보여지게 된다.
  6. URL 파라미터 받는 법: <Route path='경로명/:파라미터' component={컴포넌트이름}/> 에서 처럼 :으로 파라미터임을 선언할 수 있다. 컴포넌트에선 props.match.params.파라미터이름으로 해당 파라미터에 접근하여 값을 불러올 수 있다.
  7. Query 받는 법: 쿼리는 props로 넘어오는 값 중 locationsearch에서 값을 찾을 수 있다. 이때 qs란 모듈을 이용하면 좀 더 편하게 쿼리를 오브젝트로 변환할 수 있다.

부가 기능

  1. history: history.goBack(), history.push() 등을 통해 뒤로가기, 홈으로 이동을 구현할 수 있다. history.block(‘정말 떠나시나요’) 로 페이지 변화 발생시 경고 메시지 생성도 가능하다.
  2. withRouter: route와 연결되지 않은 컴포넌트여도 match, location, history 등을 받을 수 있도록 해주는 HoC이다.
  3. NavLink: Link의 Nav용으로 해당 링크에 맞게 CSS를 토글할 수 있다.

14. 외부 API를 연동하여 뉴스 뷰어 만들기

  1. 비동기 작업의 이해: 웹브라우저는 API 호출과 같은 시간이 걸리는 작업을 비동기로 처리한다. 즉 실행의 결과를 기다리지 않는다. 결과를 기다려야 하는 경우 콜백, Promise, Async/Await을 통해 결과를 받은 뒤 작업을 진행시킬 수 있다.
  2. axios로 API 호출해서 데이터 받아오기: axios는 비동기 HTTP 클라이언트 모듈로 const result = await axios.get(url, params) 와 같은 형태로 사용할 수 있다.
  3. 데이터 연동하기: 이때 주의할 점은 useEffect 함수 자체를 async화 시키는 것이 아닌 따로 const fetchData = async () => {}와 같이 데이터를 호출하기 위한 비동기 함수를 생성해주어야 한다. 이는 useEffect를 사용하기 위한 원칙으로 꼭 주의해야한다.
  4. usePromise 커스텀 Hook 만들기: 비동기 함수를 인자로 받아, loading, resolved, error 를 반환하는 hook을 만들어보았다.

15. Conetxt API

  1. Context API의 전역 상태 관리 흐름: 기존 리액트에선 탑다운 방식의 여러 컴포넌트를 거칠 수 밖에 없는 상태 흐름을 가지고 있다. 전역적인 상태를 더 효율적으로 관리하기 위해 Context API가 나왔고, 원하는 값을 여러 컴포넌트 거치지 않고 한번에 가져올 수 있게 되었다.
  2. 사용법 익히기
    1. const SampleContext = createContext(나만의 상태)를 통해 상태를 생성한다. 이렇게 생성된 Context는 Value, Consumer, Provider를 가진다.
    2. <SampleContext.Provider value={초기값}>...</SampleContext.Provider> 처럼 Provider를 사용할 땐 반드시 Value를 설정 해주어야 한다.
  3. 동적 Context 사용하기
    1. 동적으로 사용한다는 건 Context를 변화시킬 수도 있게 하는 것이다. 여러 방법이 있지만 교재에선 Hook을 이용하여 Context 속에 state를 수정할 수 있는 함수를 함께 넣었다. ```jsx const SampleContext = createContext({state: 2, action: setNum: ()=>{}});

    const SampleProvider = ({children}) => { const [num, setNum] = useState(9); const value = {state: num, actions: setNum}; return <SampleContext.Provider value={value}></SampleContext.Provider> } const { Consumer: SampleConsumer } = SampleContext; export { SampleProvider, SampleConsumer } ```

    1. 위와 같이 Provider와 Consumer를 분리하여 더 편리하게 상태를 관리할 수 있도록 하였다.
  4. Consumer 대안법: 하지만 이런 식으로 Consumer를 그때 그때 불러서 사용하는 것 보다 리액트에서 기본적으로 더 편리한 기능을 제공한다.
    1. useContext Hook: 함수형 컴포넌트에서 사용 가능
    2. static contextType: 클래스형 컴포넌트에서 사용 가능

16. 리덕스 라이브러리 이해하기

  1. 개념 정리
    1) 액션(Action)
    액션은 상태에 변화를 주고자 할 때 발생하는 것. 하나의 객체로 표현되며, 액션 객체는 type 필드를 반드시 가져야 한다. 2) 액션 생성 함수
    액션 객체를 만들어주는 함수. 액션 객체는 매번 직접 만드는 것보다 액션 생성 함수로 만들고 관리하는 것이 좋다. 3) 리듀서(Reducer)
    변화를 일으키는 함수. 액션 생성 후 발생시키면(2번과 5번), 리듀서는 현재 상태와 액션 객체를 받아와서, 이 값들을 참고한 새로운 상태를 반환해준다. 4) 스토어(Store)
    프로젝트의 상태에 관한 데이터가 담겨있다. 현재 상태, 리듀서, 몇몇의 내장 함수들을 지니고 있다. 5) 디스패치(Dispatch)
    액션을 발생시키는 것. 액션 객체가 파라미터로 들어간다. 스토어의 내장함수. 6) 구독(Subscribe)
    스토어의 내장함수. 컴포넌트가 스토어에 특정 함수를 전달하며 구독을 한다. 그러면 이후 액션이 디스패치되어 스토어의 상태값이 변동되었을 때 전달받았던 함수를 호출해준다.



  2. 리덕스의 3가지 규칙
    1) 하나의 프로젝트는 단일 스토어를 가진다.
    사실 불가능하지는 않으나, 권장 사항. 2) 리덕스 상태는 읽기 전용이다.
    즉, 상태를 업데이트할 때 기존의 객체는 그대로 두고 새로운 객체를 생성해주어야 한다. 리덕스에서 불변성을 유지해야 얕은 비교 검사를 통해 내부적으로 데이터가 변경되는 것을 감지할 수 있기 때문이다. 3) 리듀서는 순수한 함수이어야 하며, 따라서 다음과 같은 조건을 가진다.
    • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
    • 파라미터 외의 값에는 의존하지 않는다.
    • 이전 상태는 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어 반환한다.
    • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 같은 결과 값을 반환해야 한다.

17. 리덕스를 이용한 리액트 상태 관리

리덕스를 사용했을 때의 이점

상태 업데이트에 관한 로직을 모듈로 따로 분리한다. 컴포넌트 파일과 별개로 관리함으로써 코드 유지보수에 유용하고, 여러 컴포넌트에서 동일한 상태를 공유할 때도 유용하다.

UI 준비하기

주로 상태관리가 이루어지지 않는 프레젠테이셔널 컴포넌트와, 리덕스와 연동되어 상태와 액션을 주고 받는 컨테이너 컴포넌트로 나누어 리덕스를 사용한다.

리덕스 관련 코드 작성하기

액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 하나의 파일에 몰아 다 작성하는 Ducks 패턴을 사용할 것이다.

  1. Ducks패턴을 통해 작성한 코드를 ‘모듈’이라고 한다. 먼저 모듈 파일을 만들고, 액션 타입을 정의한다.
  2. 액션 생성 함수를 만든다.
  3. 초기 상태와 리듀서 함수를 만든다.
    • 리듀서 함수는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 역할을 한다.
    • 상태 객체에 한 개 이상의 값이 들어가면 불변성을 유지해 주어야 한다. spread연산자(…)와 배열 내장 함수(concat, filter, map 등)를 사용한다.
  4. 1~3과정으로 만든 각각의 리듀서들을 루트 리듀서로 합친다. 이때 리덕스의 유틸함수 combineReducers를 사용한다.

리액트 애플리케이션에 리덕스 적용하기

  1. createStore를 통해 스토어를 생성한다.
  2. Provider 컴포넌트로 App 컴포넌트를 감싸주고 store를 props로 전달해준다. 그러면 리액트 컴포넌트에서 스토어(리덕스)를 사용할 수 있다.
  3. Redux DevTools를 적용하여 현재 리덕스 스토어 내부의 상태를 확인할 수 있다.

컨테이너 컴포넌트 만들기

리덕스 스토어와 연동된 컨테이너 컴포넌트에서 리덕스 스토어에 접근하여 원하는 상태를 받아 오고, 액션을 디스패치한다.

  1. 컨테이너 컴포넌트를 만들고 connect 함수를 호출하여 컴포넌트와 리덕스를 연동한다. connect 함수는 상태와 디스패치할 액션을 포함한다.
  2. 만든 컨테이너 컴포넌트를 App에서 렌더링한다.
  3. connect로 받아온 상태와 액션들을 props를 통해 컨테이너 컴포넌트의 하위 컴포넌트에 적용할 수 있다.

리덕스 더 편하게 사용하기

Hooks를 사용하여 컨테이너 컴포넌트 만들기

컨테이너 컴포넌트를 만들 때 connect 함수 대신 useSelector Hook과 useDispatch Hook을 사용할 수도 있다. 단, connect 대신 hooks를 사용할 경우엔 자동으로 최적화 작업이 이루어지지 않으므로, 따로 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.


2. Q&A

질문:

App.js에 렌더링된 CounterContainer 컴포넌트에서, number를 state.counter.number와 같이 쓸 수 있는 이유는?

// containers/CounterContainer.js

const mapStateToProps = state => ({
  number: state.counter.number
});

해결:

(1)

const mapStateToProps = state => ({
  number: state.counter.number
});

mapStateToProps는 state를 파라미터로 받아오며, 이 값은 ‘현재 스토어가 지니고 있는 상태’를 가리킨다.

(2) 리듀서인 counter와 todos를 묶은 rootReducer를 사용해서 store를 생성했다. react-redux에서 제공하는 Provider컴포넌트로 App 컴포넌트를 감싸면(이때 store를 props로 전달해주어야 함), 리액트 컴포넌트에서 스토어를 사용할 수 있다.

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

(3) (2)를 통해 리액트 컴포넌트에서 스토어 사용이 가능해지므로, 현재 스토어가 지니고 있는 상태인 state에도 접근할 수 있고, 스토어 내의 리듀서(여기서는 counter)에도 접근할 수 있으며, counter 리듀서 내의 number에 접근할 수 있는 것이다. 따라서 state.counter.number로 쓸 수 있다.


질문: ColorProvider를 쓸 때의 이점은?

해결:

<ColorContext.Provider value=>

기존에는 위와 같은 식으로 코드를 썼는데, 이와 달리 ColorProvider를 쓰면 하단의 코드와 같다.

//App.js

  return (
    <ColorProvider>
        <ColorBox />
    </ColorProvider>
  );
}
//contexts/color.js

const ColorProvider = ({ children }) => {
  const [color, setColor] = useState("black");
  const [subColor, setSubColor] = useState("red");

  const value = {
    state: { color, subColor },
    actions: { setColor, setSubColor }
  };

  return (
    <ColorContext.Provider value={value}>{children}</ColorContext.Provider>
  );
};

ColorProvider를 썼을 때의 이점은 다음과 같다. 1) App.js에서 사용되는 코드가 깔끔해진다. 2) 에서는 변화시킬 value을 props로 직접 주었는데, ColorProvider에서는 변화시킬 값을 따로 관리할 수 있다. 이렇게 value를 props로 직접 전달하지 않고 따로 관리함으로써, context의 value에 상태값 뿐만 아니라 동적인 함수도 용이하게 전달해줄 수 있다. 또한 value를 state와 actions로 나누어 관리할 수 있다는 점도 ColorProvider의 장점이다. 나누어 관리하는 것은 필수사항은 아니지만 이후 편리한 context값 사용을 위해 해놓는 것이 좋다.


3. 심화 내용 정리

React 라우터

리액트 라우터는 기본적으로 match, location, histroy라는 객체를 갖고 있습니다. 특히 URL 파라미터를 사용할 때는 라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조합니다. 이 match 객체는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어 있습니다.
const data = {
  wook: {
    name: '변형욱',
    description: '리액트 스터디'
  },
 cheolsoo: {
    name: '김철수',
    description: '앵귤러 스터디'
  }
}
const Profile = ({ match }) => {
  const { username } = match.params;
  const profile = data[username];
  (...)
}
(...)
<Route path="/profiles/:usename" component={Profile} />
Profile함수는 match 객체를 props로 받는 함수입니다. 함수 내부에 정의된 username은 match.params의 값으로 사용자가 임의로 설정한 상수입니다. username은 Route path에서 path 규칙을 따라 /profile/:username으로 표기됩니다. 이렇게 설정하여 match.params.username 값을 통해 현재 username 값을 조회할 수 있습니다.
아래는 match 객체가 갖고 있는 값들입니다.

match

{
  "path": "profile",
  "url": "/profile",
  "isExact": false,
  "params": {}
}

리덕스에서 초기값, 액션생성함수 props로 받아오기

redux를 사용할 때는 액션 타임, 액션 생성 함수, 리듀서 코드를 작성합니다. 이 코드들을 각각 다른 파일에 작성할 수도 있고, 기능별로 묶어서 파일 하나에 작성하는 방법도 있습니다. 이 중 기능별로 묶어서 파일 하나에 작성하는 방법을 Ducks 패턴이라고 합니다. 이 구조는 크게 components, modules, containers의 하부 디렉토리로 구성됩니다. components는 UI를 담당하는 컴포넌트들이 포함되어 있고 modules에는 액션객체, 액션 생성 함수, 리듀서가 포함되고 containers에는 액션을 dispatch하는 컴포넌트들이 속해 있습니다.
react-redux에서는 connect 함수를 제공합니다. 이 함수는 다음과 같이 사용합니다.
connect(mapStateProps, mapDispatchProps)(연동할 컴포넌트)
여기서 mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수이고, mapDispatchToProps는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수입니다.
containers/CounterContainer.js
import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease } from '../modules/counter';

const CounterContainers = ({ number, increase, decrease }) => {
    return (
        <Counter number={number} onIncrease={increase} onDecrease={decrease)/>
    );
};

export default connect(
    state => ({
        number: state.counter.number,
    }),
    {
        increase,
        decrease
    },
)(CounterContainer);
위의 CounterContainer.js의 코드를 살펴보면 CounterContainers 컴포넌트는 number, increase, decrease를 props로 받고 있습니다. import문에서 increase와 decrease는 불러오는데 number는 ‘어디서 온 것일까?’라는 의문을 가질 수 있을 것입니다. 이는 connect 함수를 코드 본문에 따로 명시하지 않고 export default를 통해 바로 export하고 있기 때문에 나타나는 상황입니다. 위에 언급했듯이 connect 함수는 첫번째 파라미터는 리덕스 스토어에서 받아온 상태 값을 넣어 줍니다. 다시 말해, props로 받아온 상태값 number는 export default connect()를 통해 받아온 값입니다.
아래는 다른 예시입니다.
containers/TodosContainer.js
import React from 'react';
import { connect } from 'react-redux';
import Todos from '../components/Todos';
import { changeInput, insert, toggle, remove } from '../modules/todos';

const TodosContainer = ({
    input,
    todos,
    changeInput,
    insert,
    toggle,
    remove,
}) => {
    (...)
};

export default connect(
    ({ todos }) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove,
    },
)(TodosContainer);
참조 ‘리액트를 다루는 기술’