useEffect ?

React 컴포넌트가 렌더링될 때마다 특정 작업(Side effect)을 실행할 수 있도록 하는 리액트 Hook이다. 여기서 side effect란 컴포넌트가 렌더링된 후 처리되어야 하는 부수적인 효과들을 말하며, 컴포넌트가 렌더링된 이후 비동기적으로 처리되는 작업들을 일컫는다. 이를 통해 함수형 컴포넌트에서도 클래스형 컴포넌트에서 쓰던 생명주기 메소드들(ex : componentDidMount)들을 쓸 수 있게 됐다.

useEffect는 실행되면 인자로 받은 콜백을 예약해뒀다가, 컴포넌트가 렌더링되면 예약해둔 콜백을 실행한다.

 

기본적인 형태

useEffect(function, deps);

// ex
useEffect(() => {
  document.title = `총 ${count}개`;
}, [count]);

function : 실행하고자 하는 함수 즉 side effect

deps : 의존성 배열. 이 배열 안의 값이 변경되는 경우에만 side effect가 실행됨. 빈 배열을 줄 경우 해당 컴포넌트가 최초 렌더링될때만(즉 생성될 때만) side effect가 실행되고, 배열 자체를 주지 않을 경우 컴포넌트가 렌더링될때마다 side effect가 수행됨.

 

※ 여기서 말하는 컴포넌트란 useEffect가 작성된 컴포넌트를 말함

 

 

Advanced

1. deps의 종류에 따른 side effect 수행

  • deps로 뭔가 값이 들어있는 배열을 준 경우 : 컴포넌트가 마운트될때(생성될 때)와 deps배열에 작성된 값들이 변경될 때만 side effect가 실행
  • deps로 빈 배열을 준 경우 : 컴포넌트가 마운트될때만 side effect가 실행 (ComponentDidMount만 표현한 느낌)
  • deps로 아무것도 주지 않은 경우 : 컴포넌트가 마운트될때, 컴포넌트가 렌더링될 때마다 side effect가 수행됨(ComponentDidMount와 ComponentDidUpdate를 표현한 느낌)

 

※좀 더 정확히 설명하면..

  1.  컴포넌트가 마운트될 때 콜백을 예약하고 deps 리스트 안의 값들을 기억함. 이후 콜백을 실행.
  2.  state변경 등으로 컴포넌트가 리렌더링되면 useEffect가 다시 실행됨. 이 때 새로 실행되는 useEffect의 deps 리스트 안의 값들을 전에 기억해두고 있던 deps 리스트의 값들과 비교
  3.  달라진 게 있다면 새로 실행된 useEffect의 콜백을 예약하고 렌더링이 끝나면 실행함. 그러나 달라진 게 없으면 콜백을 예약하지 않음(즉 렌더링 끝나도 실행되지 않음).
  4.  때문에 deps를 빈 배열로 주면 항상 전에 기억하던 것과 같은 거니까 이후에 다시 컴포넌트가 렌더링되도 콜백을 수행하지 않음 즉 최초 마운트할 때만 실행하는 꼴이 됨. deps안에 내용물이 뭔가 있을 때, 컴포넌트가 리렌더링되도 deps값이 변한 게 없으면 콜백을 실행하지 않음 즉 deps안의 값이 변경될 경우에만 콜백(side effect)가 실행되는 꼴

 

2. 컴포넌트가 unmount될때 수행할 side effect (clean up)

: side effect로 작성한 함수에서 특정 함수를 return하게 하면 clean up기능을 사용할 수 있다. 덕분에 컴포넌트가 unmount될 시 리턴하는 함수가 실행되게 할 수 있다. useEffect를 통한 side effect는 여러 번 실행될 수 있기 때문에 메모리 누수 등을 막기 위해 이런 clean up이 필요한 경우가 많다. 참고로 컴포넌트가 unmount될 때 실행되므로 컴포넌트가 업데이트될 때(리렌더링될 때)에도 수행된다. 이 경우는 리렌더링이 먼저 된 후 clean up이 수행되고, 새로운 side effect가 실행된다. 즉 useEffect에서 clean up의 동작 순서는

 

  1. props나 state가 update됨
  2. 컴포넌트가 리렌더링됨
  3. 이전 side effect의 clean up이 수행
  4. 새로운 side effect 수행

 

이다.

 

※ clean up을 쓰는 또 다른 이유

: 클로저 때문이다. useEffect에서 side effect로 주는 함수는 자기가 생성될 때의 값을 바라보기 때문. 즉 side effect내에서 참조하는 state가 가장 최신의 state가 아닐 수도 있다. 이 때 clean up이 있다면 과거의 변수 값을 참조하던 effect가 정리되고 새로운 변수 값을 다시 참조할 수 있게 된다. 

 

 

엄청 쉽다. state를 하나 만들고, 그 값이 비동기 작업이 이루어질 땐 true로 세팅해뒀다가 비동기 작업이 끝나면 false로 세팅하게 하면 된다. 평소엔 false다가 비동기 작업을 할 때만 true가 되는 식으로!

 

const [loading, setLoading] = useState(false);

// 어떤 함수 내에서
setLoading(true);
const uploaded = await 비동기작업;
setLoading(false);

 

그 다음 loading이 true인지 false인지에 따라 다음과 같이 조건부 렌더링을 할 수 있다.

{!loading && <div>평상시</div>}
{loading && <div>로딩중</div>}

 

이를 활용해 로딩중(비동기 작업 중)일 때 로딩스피터가 빙글빙글 돌도록 만들수도 있다!

우선 로딩 여부에 따라 다음과 같이 렌더링된다고 가정하자.

 

{!loading && <div>평상시</div>}
{loading && <div className="loading"></div>}

다음과 같이 CSS를 작성해보자. animation을 활용한다!

.loading {
  /* 높이, 너비 설정 */
  width: 1.5em;
  height: 1.5em;
  /* 정사각형은 border-radius를 50프로로 하면 원모양이 됨 */
  border-radius: 50%;
  border: 3px solid grey;
  /* 원의 4분의 1은 분홍으로 */
  border-top: 3px solid pink;
  /* spin이란 애니메이션을 2초동안 일정한 속도로 수행, 이를 무한히 반복 */
  animation: spin 2s linear infinite;
}

/* spin이란 이름의 애니메이션 만들기 */
@keyframes spin {
  /* 프레임별로 지정 */
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

 

의존성 주입(dependency injection) : 어떤 함수나 클래스가 자신들이 내부적으로 사용할 기능을 자신들 내부에서(즉 스스로가) 만들어내지 않고, 외부에서 이 기능들을 만든 뒤 주입시키는 것.

 

예를 들어, app.jsx에서 유튜브 api를 호출하는 기능을 쓴다고 할 때 이 기능을 app.jsx 내에서 만들 수도 있겠지만, 외부에서 유튜브 api를 호출하는 기능을 만든 다음 app.jsx에게 주입(by props)하여 이를 쓰게 할 수도 있다. 이를 의존성 주입이라 한다. 찾아보니까 객체 지향 프로그래밍(OOP)에서 코드의 재사용성을 목적으로 잘 사용되는 패턴이라고 한다.  (MVC패턴 등에서 view에 해당하는 애들이 비즈니스 로직도 처리할 수 있고 ~~도 할 수 있고~ 그러면 곤란..) 리액트에서는 컴포넌트들의 테스트를 하는 것에도 DI를 쓰는 게 좋다고 한다. 

 

리액트에선 index.js에서 사용할 함수 등을 불러와 app.jsx에 집어넣고, 그 다음은 이를 쓰는 컴포넌트로 계속해서 전달전달하는 식으로 활용한다. 예를 들면 다음과 같다.

const authService = new AuthService();

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

// authService객체는 login등의 메소드를 사용가능한 객체이다

그러면, App컴포넌트 안에 B컴포넌트 안에 C컴포넌트 안에 D컴포넌트 안에 E컴포넌트가 있다고 해보자. 또한 E컴포넌트는 login과 logout이라는 기능을 써야 한다. 이 때 방금 한 것처럼 index.js에서 login, logout메소드를 쓸 수 있는 객체(authService라고 가정)를 만들어 app.jsx에 넣어주고, app이 이를 계속 전달전달해주는 방식을 쓸 수 있다. 

// index.js

const authService = new AuthService();

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


// app.jsx

function App({authService}){
	return <B authService={authService} />;
}


// B .jsx

function B({authService}){
	return <C authService={authService} />;
}


// C.jsx

function C({authService}){
	return <D authService={authService} />;
}


// D.jsx

function D({authService}){
	return <E authService={authService} />;
}


// E.jsx

function E({authService}){
	return(
    	<>
           <button onClick={authService.login}>로그인</button>
           <button onClick={authService.logout}>로그아웃</button>
        </>
    );
}

이 때 E에서 다른 기능들도 이용해야 한다면? 그래서 index.js에서 다른 객체를 만들어 E에 전달해야 한다면? app.jsx, B.jsx, C.jsx, D.jsx에 하나하나 직접 코딩을 해줘야 한다. 이 작업? 너무 귀찮다.

 

그래서 여기서 꼼수(?)가 하나 있다. index.js에서 애시당초에 필요한 의존성들을 집어넣은 E라는 컴포넌트를 만들어 이걸 전달하는 것이다. 다음과 같다.

const authService = new AuthService();
const youtube = new Youtube();

const Jofe = props => {
  return(
    <E {...props} authService={authService} youtube={Youtube}/>
  );
};

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

return <E authService={authService} youtube={youtube} /> 를 해도 되지만 확장성을 위해 저렇게 한 번 감싼 형태의 컴포넌트로 만들어서 전달할 수도 있다. 이렇게 하면 장점은, E에 새로 전달할 객체가 있어도 index.js에서 한 번만 수정하면 된다. 

모두들 알다시피 리액트에서 setter함수는 비동기적으로 동작한다.

즉 호출되는 순간 state를 업데이트하고 다음 코드를 실행하는 방식이 아니기 때문에 예기치 못한 오류들에 직면할 수도 있다.

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count + 1);
  console.log(count);
}

// 카운트가 0일 때 onAdd가 실행되면 콘솔에 찍히는 카운트값은 1이 아니라 0일수도 있다

위 상황에선 내가 분명 setCount를 통해 count값을 1 늘려줬는데, 콘솔로그에 찍는 코드에서 참조되는 count값은 1 늘어난 값이 아니라 옛날에 쓰던 count값이 되는 문제가 생긴다. 즉 가장 최신의 count값을 참조해야 하지만 못 하게 되는 것이다. setter함수가 비동기적으로 동작하기 때문에..(이 문제는 클래스형 컴포넌트에서는 두 번째 인자로 갱신 이후 실행할 콜백을 넘겨줌으로써 해결할 수 있고 함수형 컴포넌트에선 useEffect를 통해 해결가능했다)

 

또 문제인 것은 setCount가 여러 번, 예를 들어 100번 호출됐다면 100이 늘어나야 하는데 100보다 덜 늘어나는 상황도 생길 수 있다. 각각의 setCount함수들이 참조하는 count값이 가장 최신의 count값이라는 보장이 없기 때문.

 

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

// 카운트가 5 늘어날 것이라 예측 가능 but 5만큼 안 늘어났을 수도 있다..

 

이 때 콜백을 써줌으로써 이 문제를 해결 가능하다. 좀 더 풀어서 말하면, 바로 이전의(즉 최신의) state를 사용해 새로운 state를 계산하는 콜백을 setter함수에 전달함으로써 이 문제를 해결할 수 있다. 공식 문서를 참고해보니, 콜백을 사용하는 갱신방법(함수적 갱신)의 경우 바로 이전 state를 사용해서 새로운 state를 계산한다고 한다.

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
}

 

setCount(count + 1)는 count값을 인자로 전달하므로, 이 때 전달된 값은 나중에 가면 가장 최신의 값이 아닐 수도 있다. 

반면 setCount(count => count + 1)의 경우는 count값이 아니라 실행할 콜백을 인자로 전달하고, 전달된 콜백이 실행되면 그 때서야 그 때의 count값을 참조하므로 이 경우는 가장 최신의 count값을 참조하는 것이다.

Firebase

: Google이 소유하고 있는 모바일, 웹 애플리케이션 개발 플랫폼. 즉 앱을 개발하고 개선할 수 있는 도구들의 모음이다. 인증, 데이터베이스, 푸시메시지 등등을 지원하는 플랫폼으로 이를 활용하면 좀 더 편한 개발이 가능(그런 것들을 하나하나 만들지 않아도 되니까). 백엔드 기능을 클라우드 서비스 형태로 제공하기 때문에 서버리스 어플리케이션 개발을 가능하게 하는 녀석. 

 

공식문서를 참조하면 연동하는 법을 알 수 있으나 내 입장에선 쉽지 않았다..그래서 firebase로 구글로그인을 연동하는 법에 대해 정리한다.

 

https://firebase.google.com/docs/auth/web/google-signin?hl=ko&authuser=0 

 

자바스크립트로 Google을 사용하여 인증  |  Firebase Documentation

Check out what’s new from Firebase at Google I/O 2022. Learn more 의견 보내기 자바스크립트로 Google을 사용하여 인증 사용자가 Google 계정을 사용하여 Firebase에 인증하도록 설정할 수 있습니다. Firebase SDK를 사

firebase.google.com


 

1. 리액트 프로젝트에 Firebase 설치

yarn add firebase

 

2. firebase 콘솔에서 프로젝트를 만들고 SDK를 받는다. 

 

3. firebase 콘솔에서 인증(authentication) 섹션을 열고 로그인 방법 탭에서 구글 로그인을 사용 설정시킴.

 

4. 리액트 프로젝트에 firebase.js(이름은 뭐 자유ㅎㅎ)를 만들고 다음과 같이 코드 작성

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
  measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};

// Initialize Firebase
export const firebaseApp = initializeApp(firebaseConfig);

참고로 리액트에서 API key같은 것들을 작성할 땐 보안문제가 있으니 발급받은 거 그대로 쓰지 않는 게 좋다. 프로젝트 폴더에 .env라는 폴더를 만들고, 거기에 REACT_APP_API_KEY = 블라블라 이런 식으로 쓴 다음, 프로젝트에서 사용할 때는 process.env.REACT_APP_API_KEY이렇게 쓰면 됨. 참고로 저렇게 환경변수 이름을 쓸 땐 REACT_APP이란 걸 붙여야 하고, .gitignore에 .env를 작성하도록 한다.

 

5. auth_service.js라는 파일만들고(역시나 이름은 뭐 자유) 다음과 같이 만들어준다.

import { 
	getAuth, 
	signInWithPopup, 
	GoogleAuthProvider, 
	signOut } from "firebase/auth";
import { firebaseApp } from './firebase'; // 작성한 firebase.js를 불러오는 용도

class AuthService{
	constructor(){
		this.firebaseAuth = getAuth();
		this.googleProvider = new GoogleAuthProvider();
	}

	login(){
		return signInWithPopup(this.firebaseAuth, this.googleProvider);
	}

	logout(){
		signOut(this.firebaseAuth);
	}
}

export default AuthService;

참고로 클래스 형태가 아니라 함수로 만든 다음 그걸 export하는 형태로 만들어도 상관없다.  또한 아까 작성한 firebase.js의 코드를 실행시켜 firebase app을 초기화해야 하기 때문에 6번 줄에서 import하는 모습.

 

 

6. index.js에서 작성했던 auth_service.js를 활용해 dependency injection

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.module.css';
import App from './app';
import AuthService from './service/auth_service';

const authService = new AuthService();

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

이를 통해 app.jsx에서는 authservice.login을 통해 만들어둔 로그인 기능을 활용할 수 있다.

+ Recent posts