Chap11. useEffect, useReducer, useContext

Muru·2023년 11월 16일

[React] 지식 저장소

목록 보기
17/30
post-thumbnail

11.1 : Chap11에선 무엇을 배우는가?

  1. “Side Effects“는 무엇인지
  2. "Reducer"로 컴포넌트에서 복잡한 State를 관리하는 방법
  3. "Context"는 무엇인지



11.2 : "SideEffects"와"UseEffects"

이때까지 React는 State나 Props를 가지고 상태를 관리하거나 어떤 입력값을 받아서 화면을 구성하고, 컴포넌트를 재평가하여 리렌더링이 되고는 했다.
즉 정리하면 화면에 무언가를 가져오는 것과 관련이 있다.

“Side Effects"는 화면에 가져오는것과는 별개로 애플리케이션에서 일어나는 다른 모든 것을 의미한다.

예를들어 백앤드 서버에 http 리퀘스트를 보낸다거나 응답을 받는것은 리액트를 필요로 하지않고 리액트가 신경쓰지도 않는다. 그래서 이런 처리들은 "Side Effects"라고 할 수 있다.

그렇다면 이런 "Side Effects"를 React가 담당하도록 하는것은 안되는것이냐고 물어본다면 가능은 하지만 효율적이지도않고 “무한루프”에 빠질 가능성이 높다.

만약 HTTP 리퀘스트에 대한 응답으로 어떤 state를 변경하는 로직을 짯다고 가정해보자.
특정 State에 대한 임의의 값 입력 => HTTP 리퀘스트 요청 => HTTP 리퀘스트 응답 => 해당 State에 대한 값을 변경 => HTTP 리퀘스트 요청 => HTTP 리퀘스트 응답 =>해당 State에 대한 값을 변경 =>(Repeat...)

그래서 "Side Effects"를 다루기 위해서 사용하는 도구가 있다.
리액트 훅이라는 도구를 사용하고, 리액트 훅 중에서도 “useEffect“ 을 사용하여 관리한다.

useEffect는 두개의 인수를 사용한다.
useEffect( 함수 , 배열(의존성으로 구성) )

  • 함수 부분은 모든 컴포넌트 평가 후에 실행되는 함수다. (단 지정된 의존성이 변경된다면 실행)
  • 배열 부분은 의존성으로 구성된 배열이다.
  • 컴포넌트가 렌더링 된다고 해당 useEffect 함수가 무작정 실행되지 않는다!

11.2.1 : "Use Effects"를 사용해보자.

다음과 같이 로그인 창에서 아이디와 비밀번호를 입력하고 Login을 누른다고 가정해보자.

이 로그인 완료창이 나타나게되고 일반적으로는 웹페이지를 새로고침해도 이 상태가 유지되어야 할 것이다.

하지만 새로고침을했더니 로그아웃이 되버렸다. 정상적인 상태라고 볼 수 없다.

저런 현상이 나오는 이유는 로그인 State의 변수가 사라지기 때문인데, 리액트 state를 관리한다고 해서 대단하게 특별한것이 아니라 결국 리액트의 배경에서 자바스크립트 변수로 관리한다.

자바스크립트 변수로 관리한것은 애플리케이션을 다시 로드할때 다 사라져버린다.

해결해야하는 문제는 크게 두가지다.
1. 다시 시작해도 상태가 유지될 수 있는 공간.
2. 리퀘스트의 요청과 응답이 있어도 무한루프를 해결하는 방법.

이를 위해서는 localStorage와 useEffect를 사용한다.

  1. 다시 시작해도 상태가 유지될 수 있는 공간 => localStorage를 사용하자.

localStorage : 브라우저에서 새로고침/재접속을 하는경우에도 데이터를 기억하게 해준다
Session Storage의 저장된 데이터가 사라지는 휘발성 저장공간과 상반된다.
localStrorage에서 setItem() 함수를 사용할수 있는데. key와 벨류값을 인수로 받는다.

로그인 핸들러 로직에 localStorage를 이용하여 로그인 상태 변수를 추가해보자.

const loginHandler = (email, password) => {
   // 'isLoggedIn" 이라는 key와 "1'이라는 value를 설정하였다.
   // 만약 로그인이 성공한다면 localStorage에서 isLoggedIn : 1이라는 키값쌍을 유지한다. 
   localStorage.setItem('isLoggedIn','1');
   setIsLoggedIn(true);
  };

이정도면 충분할까?? 단순하게 컴포넌트 함수에 LocalStorage에 대한 상태를 저장해도 되는걸까??

function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const StoredUserLoggedInInformation = localStorage.getItem('isLoggedIn");
if ( StoredUserLoggedInInformation === '1'){
	setIsLoggedIn(true);
}

  const loginHandler = (email, password) => {
   // 'isLoggedIn" 이라는 key와 "1'이라는 value를 설정하였다.
   // 만약 로그인이 성공한다면 localStorage에서 isLoggedIn : 1이라는 키값쌍을 	유지한다. 
   localStorage.setItem('isLoggedIn','1');
   setIsLoggedIn(true);
  };

충분하지 않다. 왜냐하면

if ( StoredUserLoggedInInformation === '1'){
	setIsLoggedIn(true);
}

해당 코드는 컴포넌트 함수가 재평가될떄마다 실행되는 무한루프에 빠지게된다.
=> StoredUserLoggedInInformation 의 값이 1이므로 함수 문 실행
=> useState의 setIsLoggedIn를 true로 설정함으로써 컴포넌트 함수 재평가
=> 다시 위에서부터 아래로 코드문 실행이되는중...
=> StoredUserLoggedInInformation 의 값이 1이므로 함수 문 실행
=> useState의 setIsLoggedIn를 true로 설정함으로써 컴포넌트 함수 재평가
=> 다시 위에서부터 아래로 코드문 실행이되는중...
(repeat....)


  1. 무한루프를 해결하는 방법 => 리액트 훅 useEffect를 사용하자.

useEffect(함수, 의존성배열) : 의존성에 기반하여 컴포넌트의 평가후에 함수가 실행된다.

  • 첫 실행시에만 함수가 실행하도록 하려면 의존성 배열에 빈 배열을 삽입하자.
const [isLoggedIn, setIsLoggedIn] = useState(false);
  useEffect(() => {
   const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn');

   if (storedUserLoggedInInformation === '1') {
    setIsLoggedIn(true);
   }
  }, [] );

  const loginHandler = (email, password) => {
   localStorage.setItem('isLoggedIn','1');
   setIsLoggedIn(true);
  };

작동 흐름

  1. 해당 페이지의 로그인 전 상태 ( isLoggedIn이 false값임 )

  2. useEffect가 첫실행이라서 의존성이 변경되었다고 간주하여 해당 함수 실행
    storedUserLoggedInInformation 의 값이 1이 아니라서 if문을 실행하지 않음.

  3. 해당 페이지의 로그인 후 상태( setIsLoggedIn이 작동되어 isLoggedIn이 true임)

  4. 새로고침! 앱 재실행

  5. useEffect가 첫실행이라서 의존성이 변경되었다고 간주하여 해당 함수 실행
    storedUserLoggedInInformation 의 값이 1이라서 if문을 실행하였다.




11.2.2 : 조건부 의존성 배열을 가지는 "Use Effects"를 사용해보자.

useEffect()훅을 사용할때 두번째 인수로 빈 배열을 넣는다고 하면, 앱 실행시 한번만 실행되도록 하는 일종의 트릭을 사용했다.

이번에는 두번째로 넣은 인수가 변경된것이 감지된다면 useEffect()의 문을 실행하도록 하는 방법이다.

Login.js

  useEffect(() => {
   setFormIsValid (
    enteredEmail.includes('@') &&enteredPassword.trim().length >6
   );
  }, [enteredEmail, enteredPassword]);

실행 흐름
1. 앱이 실행된다.
2. Login 컴포넌트의 코드문이 useEffect문을 제외하고 모두 실행된다.
3. useEffect문을 실행할지 따진다. ( 의존성 배열에 들어간 변수들이 변경이되었는가?)
4. 변경이되었다면 해당 useEffect문을 실행한다.

아래 사진과 같이 아이디와 비밀번호가 인풋이 들어온다면 유효성 검사를한다.




11.2.3 : cleanup 함수 사용하기.

전 단원에서 배웠듯이 Input에 따른 유효성 검사를 할 수 있다.
하지만 지금같이 규모가 작으면 문제가 적을지 몰라도 프로젝트가 커진다면 문제의 여지가 있을 수 있다.
예를들어 백엔드로 http 리퀘스트를 보낸다하면 수많은 리퀘스트를 보내는것은 지양해야하는 부분임은 분명하기 때문이다.

useEffect를 사용한곳에 콘솔로그를 집어넣어서 실제로 얼만큼 실행이 되는건지 눈으로 확인해보자.

Login.js
  useEffect(() => {
   console.log("유효성을 검사하는중");
   setFormIsValid (
    enteredEmail.includes('@') &&enteredPassword.trim().length >6
   );
  }, [enteredEmail, enteredPassword]);

아이디와 비밀번호를 입력하기만 했을뿐인데 유효성검사를 29번이나 실행했다.

해당 문제는 Cleanup 함수를 사용하면 해결 할 수 있다.


SteyByStey로 차근차근 알아보자.

1. setTimeout을 설정하여 특정 문을 지연시켜보자.

Login.js
  useEffect(() => {
   setTimeout(() => {
    console.log("유효성을 검사하는중");
    setFormIsValid(
     enteredEmail.includes("@") &&enteredPassword.trim().length >6
    );
   }, 500);
  }, [enteredEmail, enteredPassword]);

하지만 위코드로만 사용한다면 단지 0.5초만 지연시키기만 할뿐이고
setTimeout 함수는 결국 모두 실행된다.
타이머가 마지막에 하나만 실행하도록 하면 해결될 문제같다.
즉 사용자가 지속적으로 타이핑을 치다가 끝내면 마지막 타이머를 제외한 나머지는 없게끔 만드는것이다.

이를 구현하기 위해서는 clean up 함수를 사용해 볼 수 있다.

2. clean up 함수를 사용해보자.

useEffect 함수에서는 첫번째 인수로 함수를 사용하는데

  • 참고로 이 함수는 사이드 이펙트를 발생시키는 사이드 이펙트 함수라고 할 수 있다.

이 함수 안에는 return 값으로 무언가를 반환할 수 있다. 단, 함수이어야 한다.
이를 clean up 함수라고 한다. 유의할점은 이 cleanup 함수는 첫번째 사이드 이펙트 함수가 실행되기 전에는 실행되지 않는다. 하지만, 사이드 이펙트가 한번이라도 실행된 이후에 의존성 배열이 변경 감지될경우 cleanup 함수가 가장 먼저 실행이된다.

실행흐름
1. 앱 실행시 첫번쨰 사이드 이펙트 함수 실행. (cleanup 함수는 실행되지 않는다.)
=> 콘솔창의 첫번째 라인 (유효성을 검사하는중)
2. input 요소에 타이핑하여 사이드 이펙트 함수를 트리거시킨다.
=> 콘솔창의 두번째 라인 (cleanup 함수 실행합니다. 가장먼저 실행!)
3. cleanup 함수가 가장 먼저 실행 된 뒤에 두 번째 사이드 이펙트 함수가 실행된다.
=> 콘솔창의 세번째 라인 (유효성을 검사하는중)

3. clearTimeout을 이용해보자.

이번에는 clearTimeout을 이용하여 완성시켜보자.
clearTimeout(식별자);
clearTimeout 함수는 setTimeout을 사용한 식별자의 실행을 취소 시킨다.

login.js

useEffect(() => {
  // setTimeout을 식별자로 두자.
   const identifier = setTimeout(() => {
    console.log("유효성을 검사하는중");
    setFormIsValid(
     enteredEmail.includes("@") &&enteredPassword.trim().length >6
    );
   }, 500);

   return () => {
  // 사이드 이펙트가 실행되기 이전에 이전 setTimeout을 취소시킨다. 
    console.log('cleanup 함수 실행합니다.');
    clearTimeout(identifier);
   };
  }, [enteredEmail, enteredPassword]);

27번의 리퀘스트(유효성검사)가 단 한번의 리퀘스트로 가능해졌음을 볼 수 있다.




11.3 : useReducer

"useReducer"는 상태를 관리한다. useState와 유사하지만,
큰 차이점은 상태가 복잡해질때는 useReducer훅을 사용하여 좀더 편리하게 상태관리를 할 수 있다.

특정 상태가 변경된다면 다른 상태도 변경되는 종속상태같은것을 손쉽게 관리가 가능하다는 것이다. 하지만 일반적인 useState보다 설정을 좀 더 해야한다.

useState => 서로 연관된 상태가 없는 독립된 상태가 많을 경우 사용
useReducer => 상태들이 서로 연관되어 있거나 종속상태가 있는경우 사용

어떤 경우에 사용해야하는지 실제 코드를 보면서 깨달아보자.

11.3.1 : useReducer을 어떨때 사용하는것이 좋을까?

첫번째 케이스 : 두 개의 다른 state를 사용할경우.
전 state의 스냅샷을 얻어와서 최신 스냅샷을 업데이트하려고 할때 함수 폼을 사용했었다.
함수 폼을 사용하는 예시

const someChange = (event) => {
setChangedSomething ( (최신상태의스냅샷의 매개변수) => { ~~의 상태로 업데이트 } )
}

그런데 다음과 같이 상태가 두개일경우도 가능할까?

const emailChangeHandler = (event) => {
   setEnteredEmail(event.target.value);
   setFormIsValid(
     event.target.value.includes("@") &&enteredPassword.trim().length >6
   );
  };

EnteredEmail의 상태와 enteredPassword의 상태를 두가지 모두 최신 스냅샷을 얻어와서 업데이트하는것이 가능한것일까?
일반적으로는 불가능하다. 또는 코드가 꼬여 이상하게 실행될 가능성이 높다.
이럴경우 useReducer를 사용하는것이다.


두번 째 케이스 : 다른 state에 의존하여 새로운 state를 도출하는 경우

  const [enteredEmail, setEnteredEmail] = useState("");
  const [emailIsValid, setEmailIsValid] = useState();
  const [enteredPassword, setEnteredPassword] = useState("");
  const [passwordIsValid, setPasswordIsValid] = useState();


// setEmailIsValid 함수는 emailIsValid 상태를 관리해야함에도 불구하고,
// setEmailIsValid 함수는 enteredEmail 상태를 관리하고 있는 모습이다.
 const validateEmailHandler = () => {
   setEmailIsValid(enteredEmail.includes("@"));
  };

// setPasswordIsValid 함수는 passwordIsValid 상태를 관리해야함에도 불구하고,
// setPasswordIsValid 함수는 enteredPassword 상태를 관리하고 있는 모습이다. 
  const validatePasswordHandler = () => {
   setPasswordIsValid(enteredPassword.trim().length >6);
  };

이런식으로 다른 상태의 state에 의존하여 새로운 state를 만들어내는 행위는 해서는 안되는 행위다.

그러면 함수폼을 사용하여 최신스냅샷을 얻어와서 새로운 상태를 만들어내는것은 가능하냐? 그것도 역시 불가능하다. 아래 코드와같이 setEmailIsValid 함수를 사용했다면

setEmailIsValid(enteredEmail.includes("@"));

최신 스냅샷을 얻어올 수 있는 상태는 오로지 emailIsValid의 상태뿐이다.




11.3.2 : "useReducer"를 사용해보자.

const [ state, dispatchFn ] = useReducer(reducerFn, initialState);

state : 최신 상태값을 리액트에게 전달받는다.

dispatchFn : state의 상태를 업데이트해주는 함수로 “정보(action)”을 인수로전달하여 호출한다면,
리액트가 useReducer 훅의 첫 번째 인수로 전달한 "reducerFn 함수를 호출“한다.
다시 정리하자면 dispatch 함수를 호출하면 리액트가 useReducer 훅 호출시 첫 번째 인수로 전달한 reducer 함수를 실행한다.

  • action은 상태를 업데이트하기 위한 정보를 의미하는데, 문자열이나 숫자 모두 가능은 하지만 일반적으로 “객체”를 전달한다. 식별자같은 역할을 하는 type이라는 프로퍼티를 통해서 여러 상황에 대한 상태 업데이트를 가능케한다.
    보통 type라는 프로퍼티명을 사용하는데 typeeee 이런식으로 내마음대로 이름을 지어봐도 상관은 없었지만 통상적으로 type이라고 명명하는것 같다.

reducerFn(state, action) => newState
reducerFn 함수의 인수중 첫번째는 가장 최신 상태인 state
reducerFn 함수의 인수중 두번째는 dispatchFn 함수 호출시 인수로 전달한 "action 객체“를 전달 받아 호출된다.
결과값은 새로운 상태를 변경한다.

initialState : state의 초기상태

동작흐름
1. dispatchFn 함수를 호출하면서 action 객체를 전달하면 리액트가 reducerFn을 호출한다.
2. 리액트가 deducerFn을 실행할 때 인수로 “가장 최근 상태”와 “action 객체”가 reducerFn 함수의 인수로 전달하면서 실행한다.
3. reducerFn 내부에서는 action 객체의 type 프로퍼티로 여러 조건을 분기별로 새로운 상태값을 반환값으로 가진다.



흔한 예제인 카운트 함수로 예시를 들어보자

App.js

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

// count의 초기값 설정
const initialState = {count : 0};

// state 매개변수는 가장 최근 상태 받으며, action매개변수는 객체를 받는다.
const thisIsReducer = (state, action) => {

// action 객체의 type 프로퍼티로 분기별로 나눠 새로운 state를 리턴한다.
  switch(action.type) {

// action 객체의 type이 'increment'일경우 최신 상태의 count에서 +1 증가시켜 count의 값을 갱신한다.
   case 'increment' :
    return {count : state.count + 1};

// action 객체의 type이 'decrement'일경우 최신 상태의 count에서 -1 감소시켜 count의 값을 갱신한다. 
  case 'decrement' :
    return {count : state.count -1 };
    default :
    throw new Error();
  }
}

function App() {
 
  // useReducer 리액트 훅을 정의한다.
 // 초기값 thisIsState = { count : 0 }; 
 const [thisIsState, thisIsDispatch] = useReducer(thisIsReducer, initialState)
  return (
   <div>
    Count : {thisIsState.count}
// 클릭 트리거가 발생한다면 dispatch함수를 호출하면서 정보들을 "action 객체"로 담아 전달하고 리액트가 자연스럽게 reducerFn을 호출한다. 
// 이때 reducerFn 함수는 첫번째 인자로 최신 상태의 state의 스냅샷의 정보를 얻고, 
// reducerFn 함수의 두번째 인자로 "action 객체“의 정보를 사용할 수 있다.
    <button onClick={() => thisIsDispatch({type:'decrement'})}>-</button>
    <button onClick={() => thisIsDispatch({type:'increment'})}>+</button>
   </div>
  );
}

export default App;

실행 결과




11.4 : Context API

이때까지 props를 통해서 부모에서 자식으로 데이터를 보내고는했다.
문제가 있는점은 다른 체인에 있는 자식에게 데이터를 보내려고할때 다이렉트로 보내지 못한다는것은 배웠다. 상향식을 통해 데이터를 전달하여 다시 하향식으로 도달하는 방식을 사용했었다.
도중에 통로를 거쳐하는 부분이 많아서 문제가 될 여지가 있고 가독성에도 좋지는 않음이 분명하다.

이때 일종의 다이렉트 통로를 만드는 "Context Api“를 사용해 문제의 여지를 개선해 볼 수 있다. 아래 사진의 노란색 상자가 다이렉트 통로의 역할을한다.

11.4.1 : "Context API"를 사용해보자.

예시로 사용할 컴포넌트들의 구조는 다음과같다

Navigation.js 에서 Login.js의 정보가 필요하다.
( Navigation 앱에서 로그인이 되었는지의 여부가 필요하다.)

기존의 방식대로 사용한다면,
Login 컴포넌트에서 상향식으로 App.js까지 데이터를 올려서 필요하고자 하는 Navigation.js 컴포넌트까지 props로 전달했다면,

이제는 Context API를 사용하여 중간단계의 컴포넌트를 거치지 않고 곧바로 전달할 수 있게끔 하는 제 3의 통로를 만들어 깔끔하게 전달해보자.

Context API를 사용하는 흐름은 다음과 같다.

  1. Context 앱을 만든다. React.createContext()
    ** 인수안에 보통 객체를 사용한다.
  2. Context 앱에 공급한다. <AuthContext.Provider value={{..}}>
    ** Context 앱에 공급하고자 하는 JSX 코드를 감싸 주면된다.
  3. Context 앱에 공급된것을 소비한다. 소비 방식에는 두가지가 있다
    3-1. AuthContext 소비자를 사용
    3-2. 리액트 훅 사용




  1. Context 앱을 만든다. React.createContext()

auth-context.js

import React from 'react';
// 컨텍스트 앱 만들기는 React.createContext(); 로
// 인수 안에는 "문자열"같은 텍스트도 넣을수 있겠으나
// 대부분의 경우에는 객체.
const AuthContext = React.createContext({
   isLoggedIn : false
});

export default AuthContext;


  1. Context 앱에 공급한다. <AuthContext.Provider value={{..}}>

app.js

import AuthContext from "./store/auth-context";
function App() {
   const [isLoggedIn, setIsLoggedIn] = useState(false);
(...)
  return (
   // React.Fragment도 없애줘도된다.
   // AuthContext 자체가 감싸주는 역할을 하기때문이다.
   //<React.Fragment>
     {/* AuthContext에 전달했으니 더이상 props를 통해서 전달할 필요가 없다. 지워주자 */}
    {/* <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} /> */}

// AuthContext JSX 코드 안에있는 모든 자식,자손 컴포넌트와 상태들을 AuthContext 앱안으로 공급할 수 있다.
   <AuthContext.Provider value={{ isLoggedIn: isLoggedIn, }}>
    <MainHeader onLogout={logoutHandler} />
    <main>
     {!isLoggedIn &&<Login onLogin={loginHandler} />}
     {isLoggedIn &&<Home onLogout={logoutHandler} />}
    </main>
   </AuthContext.Provider>


   //</React.Fragment>
  );
}

export default App;


  1. Context 앱에 공급된것을 소비한다.

3-1. AuthContext 소비자를 사용
Navigation.js

import AuthContext from "../../store/auth-context";

const Navigation = (props) => {
  return (
   {/* 소비자를 사용하게 되면 <AuthContext.consumer로 감싸준뒤 */}
   {/* Context 앱에 공급된것을 함수형태로 매개변수로 받은뒤 리턴해야한다.*/} 
   <AuthContext.Consumer>
    {(ctx) => {
     return (
      <nav className={classes.nav}>
       <ul>
	{/*원래는 props.isLoggedIn &&( ... */ 이런식으로 props로 받았었다.}  
        {ctx.isLoggedIn &&(
         <li>
          <a href="/">Users</a>
         </li>
        )}
        {ctx.isLoggedIn &&(
         <li>
          <a href="/">Admin</a>
         </li>
        )}
        {ctx.isLoggedIn &&(
         <li>
          <button onClick={props.onLogout}>Logout</button>
         </li>
        )}
       </ul>
      </nav>
     );
    }}
   </AuthContext.Consumer>
  );
};

export default Navigation;

3-2. 리액트 훅 사용
Navigation.js

// useContext 리액트 훅을 사용하므로 추가해준다.
import React, { useContext } from "react";

import classes from "./Navigation.module.css";
import AuthContext from "../../store/auth-context";

const Navigation = (props) => {
//  함수형으로 리턴하는 소비자 문법보다 훨씬 간결하다 !
//  useContext를 사용하여 인자로 AuthContext를 가져와서 ctx 상수에 저장해서 간단하게 쓰기만하면된다.
  const ctx = useContext(AuthContext);
  
  return (
   <nav className={classes.nav}>
    <ul>
     
     {ctx.isLoggedIn &&(
      <li>
       <a href="/">Users</a>
      </li>
     )}
     {ctx.isLoggedIn &&(
      <li>
       <a href="/">Admin</a>
      </li>
     )}
     {ctx.isLoggedIn &&(
      <li>
       <button onClick={props.onLogout}>Logout</button>
      </li>
     )}
    </ul>
   </nav>
  );
};

export default Navigation;

11.4.2 : 컨텍스트를 동적으로 만들기

아래 Navigation.js 코드에서 아쉬운점이 하나 있다.

Navigation.js

const Navigation = (props) => {
  const ctx = useContext(AuthContext);
  return (
   <nav className={classes.nav}>
    <ul>
     
     {ctx.isLoggedIn &&(
      <li>
       <a href="/">Users</a>
      </li>
     )}
     {ctx.isLoggedIn &&(
      <li>
       <a href="/">Admin</a>
      </li>
     )}
     {ctx.isLoggedIn &&(
      <li>
       <button onClick={props.onLogout}>Logout</button>
      </li>
     )}
    </ul>
   </nav>
  );
};
export default Navigation;

props를 쓰고있는 로그아웃부분이 신경쓰인다. 이것도 context를 사용할 수 없을까?
그럼 props로 데이터를 안받아와도 될텐데..

<button onClick={props.onLogout}>Logout</button>

=> 가능하다. 단지 Context앱에 로그아웃 부분을 공급해주고 소비하면되는부분이다.
onClick에 해당 하는 부분은 로그아웃 핸들러라는 함수를 받고 있으므로 마찬가지로 Context앱에 로그아웃 핸들러 함수 포인터를 전달해주면된다.
app.js에서 로그아웃 함수의 포인터를 onLogout의 속성으로 Context 앱에 전달한다.

app.js

<AuthContext.Provider value={{ isLoggedIn: isLoggedIn, onLogout : logoutHandler}}>

<AuthContext.Provider> 안에있는 JSX 코드중 에서 사용하고자 하는 컴포넌트에서
onLogout을 단지 사용해주면 끝이다.

navigation.js

<button onClick={ctx.onLogout}>Logout</button>

더하여 여전히 props를 사용하고있는 컴포넌트들이 있는데, 이상한것이 아니다. 사용할 목적에 맞게 props를 사용하거나 ContextAPI을 동적으로 사용해주면 되는것 뿐이다. 어느때에 사용해야 되는지는 자연스럽게 익혀질것이다.




11.4.3 : 사용자 정의 컨텍스트 제공자 구성요소 빌드 및 사용

  1. Context 앱 함수 내 기본 컨텍스트를 추가하여 편리성 높이기.
 const AuthContext = React.createContext({
   // 기본 컨텍스트를 서술하는 이유
   // IDE 자동완성기능을 활용하고자 하는 이유가 크다.
   isLoggedIn : false,
   onLogout : () => {},
});
  1. Context 앱 함수를 이용하여 하나의 패턴을 학습.

App.js 에 몰려있는 로직을 Context 앱으로 옮기고,
App.js는 단지 출력담당만 맡게끔 하는 패턴을 종종 볼 수 있기 때문에 알아두자.

App.js 컴포넌트가 훨씬 깔끔해진것을 확인할 수 있다.

App.js

import React, {useContext} from "react";

import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import MainHeader from "./components/MainHeader/MainHeader";
import AuthContext from "./store/auth-context";
function App() {

  const ctx = useContext(AuthContext);

  return (
   <React.Fragment>
    <MainHeader/>
    <main>
     {!ctx.isLoggedIn &&<Login/>}
     {ctx.isLoggedIn &&<Home/>}
    </main>
   </React.Fragment>
  );
}

export default App;

로직 관련부분은 Context가 담당한다.

auth-context.js

export const AuthContextProvider = (props) => {
   const [ isLoggedIn, setIsLoggedIn] = useState(false);
 
   useEffect(() => {
     const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");
     if (storedUserLoggedInInformation === "1") {
      setIsLoggedIn(true);
     }
    }, []);

   const logoutHandler = () => { 
     localStorage.removeItem("isLoggedIn");
     setIsLoggedIn(false);
   };

   const loginHandler = () => {
     localStorage.setItem("isLoggedIn", "1");
     setIsLoggedIn(true);
   };
   return <AuthContext.Provider
   value = {{
     isLoggedIn : isLoggedIn,
     onLogout : logoutHandler,
     onLogin : loginHandler,
   }}
   >
     {props.children} 
   </AuthContext.Provider>
}

export default AuthContext;

일단 와닿지는 않을 수 있어도 이런 패턴이 있다는것을 인지해두자.

11.4.4 : 리액트 컨텍스트 제한

  1. 재사용 가능성이 있는 컴포넌트의 경우 리액트를 사용하면 부적절하다.
    여기저기 다른용도로 쓰이는 Button 컴포넌트가 있다고 하자. 버튼 컴포넌트는
    로그인으로도 쓸수있고, 로그아웃으로도 쓸수있고, 폼 제출용으로도 쓸 수 있다.
    그런데 리액트 컨텍스트를 사용하여 로그아웃 부분에 정의했다고 하면 ,
    해당하는 리액트 컨텍스트로 정의한 버튼 컴포넌트는 로그아웃밖에 하지못한다.
    따라서, 구성을 따로 하려면 props를 사용하는것이 좋다.
    컴포넌트 또는 전체 앱에서 state를 관리하고자 한다면 컨텍스트를 사용한다.

  2. 변경이 잦은 컴포넌트의 경우 리액트 컨텍스트를 사용하면 부적절하다.
    1초에 여러번 바뀌거나, 잦은 변경이 있을경우 사용하면 좋지 않다고 한다.

  3. 그렇다면 전체 앱에서 여러번 바뀌는 상황이라면 어떻게 해야할까?
    리덕스를 사용하면 된다. ( 추후 학습예정 )

11.4.5 : 'Hooks의 규칙“ 배우기

리액트 훅 : use로 시작하는 모든 함수.
1. 리액트 훅은 리액트 함수에서만 호출해야 한다.
1-1 : 리액트 컴포넌트 함수에서 사용 ( 지금까지 해온것 )
1-2 : 사용자 정의 훅에서 사용 ( 추후 학습 )

예를들어서
컴포넌트에서 Reducer 함수를 정의하는곳에 useState를 사용할 수 있을까?

Login.js

const emailReducer = (state, action) => {

  // IDE에서  사용할 수 없다고 경고한다.
  useState();
  if ( action.type === 'USER_INPUT') {
   return { value: action.val, isValid: action.val.includes('@')};  
  }
   if ( action.type === 'INPUT_BLUR') {
   return { value: state.value , isValid: state.value.includes('@') };
  }
  return { value: "", isValid: false }; 
};
  1. 리액트 훅은 최상의 수준(Top Level)에서만 호출해야한다.
  • 중첩 함수에서 훅을 호출하지 말자.
  • block 문에서 호출하지 말자.

함수 컴포넌트 내부에서 사용해보자.
Login.js

const Login = (props) => {
(...)
  useEffect(() => {
  // useContext 함수를 중첩함수로 사용했지만 실행하지 못한다.
  useContext();
   const identifier = setTimeout(() => {
    console.log("유효성을 검사하는중");
    setFormIsValid(
     emailIsValid &&passwordIsValid
    );
   }, 500);
}

다음과 같이 오류 구문이 나온다.

React Hook "useContext" cannot be called inside a callback.

if문 내부에서도 마찬가지로 사용하지 못한다.
Login.js

const Login = (props) => {
   if(true) {
    useContext();
   }
}

다음과 같이 오류 구문이 나온다.

React Hook "useContext" cannot be called inside a callback.

useEffect만의 룰 : 참조하는 모든 항목을 의존성으로 useEffect 내부에 추가해야한다.

Login.js

useEffect(() => {
   const identifier = setTimeout(() => {
    console.log("유효성을 검사하는중");
    setFormIsValid(
     emailIsValid &&passwordIsValid
    );
   }, 500);
   return () => {
    console.log('cleanup 함수 실행합니다.');
     clearTimeout(identifier);
   };
  }, [emailIsValid, passwordIsValid]);

emailISValid , passwokrdIsValid는 브라우저에서 온것이 아니라 컴포넌트 함수 외부에서 오는 데이터들 이므로 의존성 배열로 추가해야한다.

  • setTimeout은?? => 브라우저 API이므로 의존성 배열로 추가할 필요가 없다.
  • setFormIsValid은?? => 브라우저 API가 아니라서 원래라면 의존성 배열에 넣는게 맞을것이다. 또한 넣는다고 해도 똑같이 실행이된다.
    하지만, 생략해도 문제가 없다. useReducer 또는 useState에 의해 노출된 state 업데이트 함수는 변경되지 않도록 리액트가 자체적으로 보장을 하므로 의존성 배열에 추가할 필요가 없다는것.

그러나 일일이 외우지 않아도된다. IDE에서 미리 경고를 해주기때문에
이를 인지해두자.

Login.js

useEffect(() => {
   const identifier = setTimeout(() => {
    console.log("유효성을 검사하는중");
    setFormIsValid(
     emailIsValid &&passwordIsValid
    );
   }, 500);

 
   return () => {
    console.log('cleanup 함수 실행합니다.');
     clearTimeout(identifier);
   };
// passwordIsValid 의존성 배열을 넣지 않았다.
  }, [emailIsValid]);

IDE에서 이에 대해 의존성배열을 추가하지 않았다고 알려준다.

React Hook useEffect has a missing dependency: 'passwordIsValid'
profile
Developer

0개의 댓글