React 기초 정리 8편

정인아·2022년 8월 31일
0

오랜만에 돌아왔습니다.
이렇게 오랜 시간 걸린 이유는....

회사가 바빴습니다..

현재 진행중인 프로젝트 마감기간도 얼마 안남았고...
새로운 팀장님의 컨벤션에 맞춰서 작업을 하다보니..
적응시간도 오래 걸리고...

여튼 그랬습니다.
이제라도 좀 꾸준하게 포스팅을 해볼까합니다.
이제 몇편 남지 않은 리액트도 빠르게 정리하고, 그동안 정리하고 싶었던 주제를 가지고 많이 포스팅 해보겠습니다.

8편은 useReducer와 ContextAPI에 대해서 얘기해볼까 합니다.

useReducer

useReducer는 react에 내장된 훅입니다.
state관리를 도와주죠.

state관리를 도와주는 훅은 따로 있었죠.
바로 useState입니다.
이 아이와 약간 흡사한 면이 있습니다.

그치만, useReducer가 더 많은 기능을 내장하고 있고, 더 복잡한 state에 유용하게 사용됩니다.

그럼 언제 useReducer를 사용하는가?

여러가지 state가 존재하고 state가 서로 종속이 많이 걸려있어서 관리가 힘들 때, useReducer를 사용합니다.

그렇다고 항상 useReducer를 사용하는건 아닙니다.
사용법이 useState보다 복잡하여, 오히려 useReducer를 사용하지 않는 분들도 있어요

아마 redux를 사용해보신 분들은, 많이 익숙하실겁니다.
redux에서 많이 사용되거든요.
오늘은 redux없는 리액트에서의 사용법을 보겠습니다.

import React, { useState } from "react";

export default function App() {
  const [email, setEmail] = useState("");
  const [isValid, setIsValid] = useState(false);

  const onChangeEmail = (e) => {
    setIsValid(e.target.value.includes("@") && e.target.value.trim().length > 6)
  };

  return (
    <form>
      <input value={email} onChange={(e) => onChangeEmail(e)} />
      <button>Click</button>
    </form>
  );
}

위의 코드를 예시로 볼게요.

위의 코드는 유효성 검사를 위한겁니다.

onChange 함수로 인해, 입력된 값에 @가 포함돼있고, 길이가 6자 넘어가면 유효성 통과로 했습니다.
물론 이메일 유효성 체크 코드는 따로 있습니다. 여기서는 간략하게만 할게요

그런데 이렇게 두가지의 state가 서로 종속입니다.
email state가 조건을 만족하게 되면, isValid state가 변하죠.

이럴 경우, useReducer를 사용 할 수 있습니다.

import React, { useReducer, useState } from "react";

const emailReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }
  return { value: "", isValid: false };
};

export default function App() {

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: false,
  });

  const onChangeEmail = (e) => {
    dispatchEmail({ type: "USER_INPUT", val: e.target.value });
  };

  return (
    <form>
      <input value={email} onChange={(e) => onChangeEmail(e)} />
      <button>Click</button>
    </form>
  );
}

위의 코드를 보실까요.
뭔가 useState와 비슷하면서 다른 부분이 있습니다.

useReducer 또한, 두가지 인자를 가진 배열을 반환합니다.
state값과, dispatch입니다.

state는 useState가 반환하는 배열의 첫번째 인자와 똑같습니다.
그럼 dispatch는 무엇일까요?

dispatch또한 setState와 똑같지만, dispatch는 useReducer의 두번째 인자로 넣은 함수를 실행시킵니다.

위의 코드에서는

  const emailReducer = (state, action) => {
    if (action.type === "USER_INPUT") {
      return { value: action.val, isValid: action.val.includes("@") };
    }
    return { value: "", isValid: false };
  };

입니다.

이 함수를 잘 보시면, state와 action 두가지 파라미터를 받죠.
state는 초기값입니다.
네, 그냥 기본적으로 여러분이 생각하는 state죠.

action은 dispatch에 넣은 객체를 그대로 받아옵니다.

그래서 이 객체에 대해서 정의를 해야합니다.

  const onChangeEmail = (e) => {
      dispatchEmail({ type: "USER_INPUT", val: e.target.value });
    };

onChange 함수를 보면 type, val를 키값으로 갖는 배열을 파라미터로 넘깁니다.

이거는 약속입니다.
type은 어떤 액션을 취할지 고유한 string으로 정의하고, val은 액션의 결과값을 반환한다고 생각하시면 됩니다.

type 키값은 모두 공통으로 쓰자고 약속했지만, val 키 값은 개발자 개인 취향에 따라 바뀝니다.

그럼
{ type: "USER_INPUT", val: e.target.value }
를 살펴보면,

USER_INPUT이라는 액션을 취하면 그 결과로 e.target.vlaue를 반환하겠다.

라고 이해하시면 됩니다.
dispatch를 실행하면,

useReducer의 두번째 인자 함수가 실행된다고 했죠?

  const emailReducer = (state, action) => {
      if (action.type === "USER_INPUT") {
        return { value: action.val, isValid: action.val.includes("@") };
      }
      return { value: "", isValid: false };
    };

이제 여길 보시면 if안에 action.type이 USER_INPUT일 경우 분기처리를 하게 됩니다.
value에 action.val를 넣게되죠.

물론 return 값도 개발환경에 맞춰 반환하면 됩니다.
중요한건 type입니다.
어떤 액션을 취할지, 그리고 그 액션에 맞는 return을 개발자가 지정해주는 거죠.

action이 많아지면 모두 if로 분기 처리하나요?
물론 그래도 되지만, 분기처리해야하는 분기점이 많이 존재할 경우 성능에 맞춰 잘 사용 할수 있는 switch가 있습니다.

이를 활용하여 reducer 함수를 잘 정리하면 가독성과 성능을 한번에 잡을 수 있습니다.

import React, { useReducer, useState } from "react";

const emailReducer = (state, action) => {
 
  swtich(action.type){
  	case "USER_INPUT" :
    	return { value: action.val, isValid: action.val.includes("@") };
    default : 
    	return { value: "", isValid: false };
  	}
  }
  
};

export default function App() {

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: false,
  });

  const onChangeEmail = (e) => {
    dispatchEmail({ type: "USER_INPUT", val: e.target.value });
  };

  return (
    <form>
      <input value={email} onChange={(e) => onChangeEmail(e)} />
      <button>Click</button>
    </form>
  );
}

위와 같이 말이죠.

useRedcer의 첫번째 인자는 무엇일까요?
그냥 간단하게 초기값입니다.

이렇게 되면 useReducer에 관한 정리는 1차적으로 끝납니다.

더 깊이 있는 공부를 위해서는 redux와 같이 하면 좋기에 이 부분은 훗날을 기약하겠습니다.

ContextAPI

지난번에 부모의 컴포넌트에서 자식 컴포넌트로 데이터를 넘겨 줄 수 있다고 했던 말 기억하시나요?
그리고 우리는 이것을 props라고 부르기로 했습니다.

그렇다면, 자식의 자식의 자식의 자식 컴포넌트에 데이터를 보내주고 싶을때는 어떻게 해야할까요?

마땅한 방법이 안떠오르시는 분들은, 자식에게 props로 넘기고, 그 자식의 자식에게 또 props를 넘기고 계속해서 넘겨주겠죠.

저희는 이런한 현상을 props drilling이라고 합니다.
드릴 처럼 props가 상단에서 내려온다는 뜻이죠.

이렇게 props을 내려줘야하는 경우는 언제 있을까요?

대표적인 예를 들어, 로그인 처리상태 입니다.
isLogged 라는 state를 만들어 비로그인일때는 false로, 로그인 상태일때는 true로 값을 저장시켜놓고 전역에서 로그인 여부에 따라 조건부 렌더링를 해줘도되고, 비로그인 유저가 마이페이지 같은 페이지에 접근하지 못하게 막아야합니다.

App.js에서부터 최하위 컴포넌트까지 isLogged라는 state를 props으로 넘겨줘야합니다.

중간에 state를 사용하지 않고 단지 전달만하는 컴포넌트도 존재 할 것입니다.

굉장히 비효율 적이죠.
지금이야 isLogged라는 state라고 예시를 든거지만, 상당히 많은 데이터를 가지고 props로 넘겨준다면 추적도 힘들어집니다.
App.js에서부터 타고 내려오면서 확인해야합니다.

그렇기에 우리는 컴포넌트 전체에서 사용가능한 리액트에 내장된 내부 state 저장소를 사용할 것입니다.

우리는 이것을 Context라고 부릅니다.

Context에 저장된 state를 어떠한 컴포넌트에서라도 직접 변경하여, 다른 컴포넌트에 다이렉트로 전달할 수 있게 해줍니다.

이제 사용법을 알아볼게요!

먼저 root 폴더에 store라는 폴더를 만듭니다.
왜 이름이 store냐, 사실 redux를 배우면서 또 거치겠지만, 먼저 말씀드리자면 state를 관리하는 장소입니다. 근데 page단위로 사용하는 state보다는 전역에서 사용하는 state를 의미합니다.

그리고 해당 폴더안에 로그인상태에 관한 state를 관리할테니 auth-context라는 파일을 추가하겠습니다.

	ㄴsrc
    ㄴstroe
      ㄴauth-context.js
    ㄴApp.js

위와 같이 말이죠. 이름을 사실 본인 마음대로 하시면 됩니다.


	import React from "react";
    
    const AuthContext = React.createContext({
      isLogged: false
    });  // 기본 context 생성하여 해당 객체를 AuthContext에 저장
    
    export default AuthContext;
    

위와 같은 코드로 auth-context.js 파일을 작성합니다.
이때 AuthContext는 isLogged라는 state를 저장한 객체입니다.

export 시켰다는거는 어디선가 import된다는 말이겠죠?

isLogged가 필요한 특정 페이지 또는 섹션에 해당 객체로 컴포넌트처럼 감쌉니다.

여기서 문제!
컴포넌트처럼이라고 했지, 컴포넌트라고 하지 않았습니다.
AuthContext는 객체입니다. React.createContext로부터 만들어진 인스턴스입니다.
그렇기에 AuthConetext는 Provider라는 키값을 가지게 됩니다.


	return (
    	<AuthContext.Provider value={{isLogged: false}}>
        	<isLogged가 필요한 컴포넌트 />
        </AuthContext.Provider>
    );

그래서 위와같이 Provider로 감싸주는 겁니다.
그리고 이 Provider는 컴포넌트입니다.
Provider로 감싸진 컴포넌트와 그 자식 컴포넌트, 그자식의 자식 컴포넌트도 이제 해당 Context에 접근 가능합니다.

그렇다면 Provider로 감싸진 어느 불특정 부분에서 어떻게 state를 불러올까요?

이 때도 AuthContext를 사용합니다.

	const isLogged가필요한컴포넌트 = () => {
    	return (
        	<AuthContext.Cosumer>
            	{(ctx) => {
                	return (
                    	<p>{ctx ? "로그인" : "비로그인"}</p>
                    )
                }}
            </AuthContext.Cosumer>
        );
    }

위와같이 불러옵니다. provider로 감쌌다면, Cosumet를 통해 불러 올 수있는데, Consumer는 함수를 child로 받습니다.

useContext

Provider는 여러분들이 redux, styled-components등과 같은 라이브러리를 사용하시게 되면 index.js에서 사용해야할 개념입니다.
Provider를 감싼 영역에서 해당 라이브러리가 힘을 발휘하는거죠.

consumer같은 경우는 사실 비효율적입니다.
이미 컴포넌트의 jsx안에서 다시 Cosumer로 호출하려하니, 사용하기에도 불편합니다.

이런 불편함을 덜어주기 위해 저희는 useContext를 사용합니다.

	import React, { useContext } from "react";
    import AuthContext from "../store/auth-context";
    
    const main = () => {
    	const ctx = useContext(AuthContext);
    	    
        return <p>{ctx ? "로그인" : "비로그인"}</p>
    }

자, 이제 이번 정리편을 마지막으로 리액트 기본 정리는 마치려고합니다.
사실 누군가에게 좋은 자료로 제공하기 보다는 제 스스로 점검해보고 싶었기에 시작했지만, 어느 순간부터 다른 이들에게도 공유되어 좋은 자료로 쓰이면 좋겠다는 생각이 들었습니다.
이 글을 읽은 개발자들이 많지는 않더라도 단 한분에게라도 괜찮은 자료로만 쓰여졌다면 너무 감사하고 만족합니다.

다음에는 어느 주제로 정리 할지 좀더 고민은 해봐야겠지만, 다음에도 기회되시면 많은 관심 부탁드립니다.

감사합니다.

profile
프론트엔드 개발자

0개의 댓글