surf react : hooks (useReducer) - 4, context API

Dino_·2021년 8월 19일
0

surf react

목록 보기
9/9
post-thumbnail

🔔 Goal

  • useReducer를 이해한다.
  • context API를 이해한다.

🌳 useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState의 대체 함수라고 생각하면 된다.

(state, action) => newState의 형태로 작성된 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환한다.

🍃 왜 쓰는거지?

  • 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.

상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있다.

공식 문서에는

다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 다음 state가 이전 state에 의존적인 경우에 보통 useState보다 useReducer를 선호합니다.

다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우를 컴포넌트 외부에 작성할 수 있다는 의미에서 로직 분리로 수렴하는 하는 것 같은데, 다음 state가 이전 state에 의존적인 경우... 리액트를 공부한지 얼마 안되서 무슨 의미인지 잘모르겠다.

useEffect나 useMemo와는 달리 deps 배열이 없는 것과 관련이 있는 것이 아닐까 하는 생각과 reducer(state, action)의 state가 현재의 state를 가져와서가 아닐까 하는 생각이 있다.

🍃 예를 들어

대표적인 카운터 예시가 있다.

import React, { useState } from 'react';

/*
Counter.js
@ feature: +, - 버튼을 활용하여 초기화된 값을 +1, -1을 해주는 기능
*/
function Counter() {
  const [number, setNumber] = useState(0);

  const onIncrease = () => {
    setNumber(prevNumber => prevNumber + 1);
  };

  const onDecrease = () => {
    setNumber(prevNumber => prevNumber - 1);
  };

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

위 코드를 보면 우리가 state를 변경시키는 로직들은 전부 Counter 컴포넌트 내부에 작성되어 있다.

만약, 내가 관리해야할 녀석이 저 값뿐만이 아니라면? 다른 자식 컴포넌트들에게도 넘겨줘야할 다양한 로직들이 있다면?

렌더시킬 녀석을 배꼽만한데, 여기에 넣어줘야할 로직이 배가 된다.

이 때 우리가 사용하면 좋은 (권장) hook이 바로 useReducer이다.

코드를 보자.

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

아직 useReducer를 사용하는 방법을 작성하지 않았지만, 겉으로 봐도 Counter 컴포넌트는 아주 군더더기 없어졌다.

🍃 하나씩 살펴보자.

초기 선언은 다음과 같다.

import React, {useReducer} from 'react';

function reducer(state, action) {
 	... 
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState, init);
  ...
}

여기의 주인공은 reducer 그리고 dispatch가 된다.

각 역할을 작성하면 다음과 같다.
const [ 상태 , 액션을 발생시키는 함수 ] = useReducer( reducer 함수 , 초기 상태, 초기 상태를 반환하는 함수)

주인공들을 살펴보면

  • reducer 는 아래와 같이 선언해주는데 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.
function reducer(state, action) {
  // ...logic
  return nextState;
}
  • dispatch는 reducer라는 녀석을 동작시키는 녀석이다.

카운터를 예시로든 코드를 보면 어떤 이벤트가 발생할 때, dispatch를 type을 포함한(필요로 하는 프로퍼티를 더 추가할 수 있다.) 객체를 넘겨주며 호출하는데, 이 객체는 reducer의 action param으로 전달된다.

추가적으로

  • initialState는 우리가 초기로 지정할 state를 저장한다.
  • init은 초기 state를 반환해주는 함수를 만들 때 사용한다.

마지막으로 useState의 대안일 뿐이지, 내가 개발하고 있는 상황에 따라 그냥 써도 되고 안써도 된다는 걸 알아두자.

🌳 Context API

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.

🍃 왜 쓰는거지?

React의 특성 중 하나인 단방향 데이터 바인딩을 알고 있다면 이해할 수 있다고 생각한다.

부모에서 자식에게 props를 전달하는 과정에서 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 그리고 깊은 레벨에 있는 자식이 props를 전달받아야 하는 경우 불필요하게 props를 전달만 받게 되는 상황이 있다.

context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있다.

🍃 예를 들어

공식 문서의 예시가 간결한 것 같아서 참고한다.

class App extends React.Component {
  render() {
    {/*App => Toobar*/}
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // 1. Toolbar 컴포넌트는 불필요한 테마 prop를 받아서 ThemeButton에 전달한다.
  // 2. 앱 안의 모든 버튼이 테마를 알아야 한다면 ?
  return (
    <div>
      {/*Toolbar => ThemedButton*/}
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    {/*이제서야 받은 props*/}
    return <Button theme={this.props.theme} />;
  }
}

Toolbar는 theme이 필요하지도 않은데 그저 전달용이 되었고, 애플리케이션 그리고 테마에 버튼이 하나만 있는 경우는 거의 없을 것이다.

이 때 Context를 사용하면 해결할 수 있다.


// light를 기본값으로 하는 테마 context를 만들어 봅시다.
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Provider를 이용해 하위 트리에 테마 값을 보내줍니다.
    // 아무리 깊숙히 있어도, 모든 컴포넌트가 이 값을 읽을 수 있습니다.
    // 아래 예시에서는 dark를 현재 선택된 테마 값으로 보내고 있습니다.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 이젠 중간에 있는 컴포넌트가 일일이 테마를 넘겨줄 필요가 없습니다.
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 현재 선택된 테마 값을 읽기 위해 contextType을 지정하고
  // React는 가장 가까이 있는 테마 Provider를 찾아 그 값을 사용한다.
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

이렇게 context를 사용하면 모든 컴포넌트를 일일이 통하지 않고도 원하는 값을 컴포넌트 트리 깊숙한 곳까지 보낼 수 있다.

🍃 API들을 하나씩 살펴보자.

React.createContext

const MyContext = React.createContext(defaultValue);

Context 객체를 만든다. Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽는다.

defaultValue 매개변수는 트리 안에서 Provider를 찾지 못했을 때만 쓰이는 기본값이다.

Provider를 통해 undefined을 값으로 보낸다고 해도 구독 컴포넌트들이 defaultValue 를 읽지는 않는다.

Context.Provider

<MyContext.Provider value={/* 어떤 값 */}>

Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 한다.

Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달한다. 값을 전달받을 수 있는 컴포넌트의 수에 제한은 없다.

Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시된다.

Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 된다. Provider로부터 하위 consumer(.contextType와 useContext을 포함한)로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트된다.

Context.Consumer

<MyContext.Consumer>
  {value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>

context 변화를 구독하는 React 컴포넌트이다. 이 컴포넌트를 사용하면 함수 컴포넌트안에서 context를 구독할 수 있다.

Context.Consumer의 자식은 함수여야 한다. 이 함수는 context의 현재값을 받고 React 노드를 반환한다.

이 함수가 받는 value 매개변수 값은 해당 context의 Provider 중 상위 트리에서 가장 가까운 Provider의 value prop과 동일하다.

상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일하다.

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* MyContext의 값을 이용한 코드 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* ... */
  }
}

MyClass.contextType = MyContext;

React.createContext()로 생성한 Context 객체를 원하는 클래스의 contextType 프로퍼티로 지정할 수 있다.

이 프로퍼티를 활용해 클래스 안에서 this.context를 이용해 해당 Context의 가장 가까운 Provider를 찾아 그 값을 읽을 수 있게 된다. 이 값은 render를 포함한 모든 컴포넌트 생명주기 매서드에서 사용할 수 있다.

이 API를 사용하면 하나의 context만 구독할 수 있다.

Context.displayName

Context 객체는 displayName을 설정할 수 있다. React 개발자 도구는 이 문자열을 사용해서 context를 어떻게 보여줄 지 결정해준다.

예를 들어, 아래 컴포넌트는 개발자 도구에 MyDisplayName로 표시된다.

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

🍃 고려해야할 점

context의 주된 용도는 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것이다.

하지만, context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 써야 한다.

여러 레벨에 걸쳐 props 넘기는 걸 대체하는 데에 context보다 컴포넌트 합성이 더 간단한 해결책일 수도 있다.

예를 들어 여러 단계 아래에 있는 Link 와 Avatar 컴포넌트에게 user 와 avatarSize 라는 props를 전달해야 하는 Page 컴포넌트를 생각해보자

<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

이렇게 props를 분리해서 각각 넣지 않고 컴포넌트 자체를 값으로 만들어서 전달할 수 있다.

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 이제 이렇게 쓸 수 있습니다.
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}

하지만 깊은 레벨의 컴포넌트에게 전달하는 것은 해결하지만 이 외에 트리 안 여러 레벨이 있는 많은 컴포넌트에 주어야 할 때도 있다. 이런 경우를 모두 컨트롤할 수 없을 때 Context를 사용하는 것을 추천한다.

🍃 주의사항

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

리액트는 다시 렌더링할지 여부를 정할 때 참조(reference)를 확인하기 때문에, Provider의 부모가 렌더링 될 때마다 불필요하게 하위 컴포넌트가 다시 렌더링 되는 문제가 생길 수도 있다.

위 코드처럼 value가 바뀔 때마다 매번 새로운 객체가 생성되므로 Provider가 렌더링 될 때마다 그 하위에서 구독하고 있는 컴포넌트 모두가 다시 렌더링 될 것이다.

이를 피하기 위해서는 값을 부모의 state 끌어올리기를 사용해야 한다.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state.value}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

🌳 Reference

profile
호기심 많은 청년

0개의 댓글