개발을 하다가 컴포넌트가 렌더링될 때 2번 렌더링되는 현상을 목격했다. 사실 이전에 다른 플젝을 할 때도 렌더링이 두 번되는 경우를 종종 봤었는데(useEffect로 예약걸어둔 함수가 두 번 실행되는..) 이번에 이게 왜 이러지 라는 생각이 들어서 알아봤다.

 

일단 결론부터 말하자면, index.js에서 React.StrictMode가 쓰여서 그렇다. 때문에 StrictMode가 적용된 걸 주석처리해주면 2번 렌더링되는 현상을 해결할 수 있다.

 

https://ko.reactjs.org/docs/strict-mode.html#gatsby-focus-wrapper

 

Strict 모드 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

이 놈은 개발모드에서만 활성화되는 녀석으로 UI적인 렌더링은 없고, 자식들에 대한 부가적인 검사와 경고를 활성화하는 것을 통해 개발에 도움을 주는 모드라고 한다. 

위 공식문서에 쓰여있다시피, StrictMode가 적용돼있으면 두 번 렌더링을 하여 생명주기 메소드들과 관련된 쉽게 오류들을 찾을 수 있게끔 도와주는거라고 한다.

 

한마디로 두 번 렌더링되는 건 내가 오류없이 잘 코딩하고 있다면 문제될 얘기가 아니라는 뜻인 듯.

순수 JS에서, 

 

  • input 이벤트 : 사용자가 input태그에 뭔가 입력할 때마다 발생하는 이벤트. 빈 input태그에 "abcd"라는 문자열을 입력했다면 a, b, c, d를 입력할 때 각각 input이벤트가 발생한다.
  • change 이벤트 : input태그의 값이 변했을 때 발생하는 이벤트. 입력이 끝나기 전과 후를 비교해 값이 변했을 때 발생한다. input태그에 원래 있던 값이 "a"였다가 "ab"로 바뀔 때 등등. 이 때 "사용자의 입력이 끝났다"로 인식되는 건 인풋 태그의 포커스가 풀릴 때, 사용자가 Enter를 입력했을 때 등등이 있다

 

근데 React에선 사용자가 뭔가 입력할 때마다의 이벤트를 받으려면 onInput으로 하지 말고 onChange로 해야 한다. 리액트 개발자들이 onChange가 좀 더 직관적이라고 생각해서 그렇게 한 거라고 함.

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가 정리되고 새로운 변수 값을 다시 참조할 수 있게 된다. 

 

 

상태 코드

: 클라이언트가 보낸 request의 처리 상태를 response에서 알려주는 것. 100번대 코드부터 500번대 코드까지 종류는 다양하며 각 번호대별로 다음과 같은 의미를 지닌다.

 

  • 1XX (Informational) : 요청이 수신되어 처리중
  • 2XX (Successful) : 요청이 정상적으로 처리됨
  • 3XX (Redirection) : 요청을 완료하려면 추가적인 행동이 필요함
  • 4XX (Client Error) : 클라이언트 측의 문제로 서버가 요청을 수행할 수 없음
  • 5XX (Server Error) : 서버 측 문제로 요청을 정상적으로 처리할 수 없음

 

만약, 클라이언트가 인식할 수 없는 상태 코드 즉 잘 모르는 상태 코드를 서버가 반환한다면, 클라이언트는 그걸 상위 상태코드로 해석해서 처리한다. 예를 들어 299번같은 상태코드는 "아 그냥 2XX : Successful로 뭉퉁쳐서 처리해야지~"하는 거다.


2XX - Successful

: 클라이언트의 request를 성공적으로 처리함을 알리는 상태코드.

 

  • 200 OK : 요청 성공을 의미
  • 201 Created : 요청을 성공해서 새로운 리소스가 생성됨을 의미. response message의 Location field에 새로 만들어진 리소스의 경로가 적힘
  • 202 Accepted : 요청이 접수됐으나 처리가 완료되지 않음을 의미. 예를 들면 요청 접수 후 1시간 뒤에 배치 프로세스가 요청을 처리하는 경우 등..
  • 204 No content : 서버가 request를 성공적으로 처리했으나 response message의 body에 넣어보낼 데이터가 없을 때. 예를 들어 save버튼을 눌러도 그 결과로 아무 내용이 없어도 괜찮을 때 등등..이 땐 결과 내용이 없어도 204 메시지만으로 성공여부를 식별 가능

 

3XX - Redirection

: 요청을 완료하기 위해 client의 추가적인 조치가 필요할 때

 

※ Redirection?

웹 브라우저는 3XX 응답의 결과에 Location 헤더가 있으면 그 위치로 자동적으로 이동되는데 이를 Redirect라고 한다. 예를 들어 client가 /event를 요청했지만 서버 입장에선 /event를 더 이상 안 쓰고 /new_event를 쓴다고 할 때, 서버는 3XX 응답과 함께 클라이언트를 /new_event로 redirect시킬 수 있다. 클라이언트 입장에선 브라우저가 지가 알아서 URL을 입력하고 엔터를 누른 느낌과 비슷하다. Redirection의 종류는

 

1. 영구 리다이렉션 : 특정 리소스의 URI가 영구적으로 이동. 예를 들면 /members가 아니라 /users를 쓰기로 한 경우, /members로 들어오면 /users로 리다이렉트시킨다.

  • 301 Moved Permanently : 리다이렉트 시 요청 메소드가 GET으로 변함. 따라서 본문이 제거될 수 있음(앵간하면 제거됨)
  • 308 Permanent Redirect : 301과 기능은 같으나 리다이렉트 시 기존의 요청 메소드와 본문을 유지함(처음에 POST로 보냈었으면 리다이렉트도 POST로)

2. 일시 리다이렉션 : 일시적인 변경 즉 잠깐 이동시키는 리다이렉션. 예를 들면 주문 완료 후 주문 내역 화면으로 이동하는 리다이렉션 등이 있다. 

  • 302 Fount : 리다이렉트 시 요청 메소드가 GET으로 변하고 본문이 제거될 수 있음
  • 307 Temporary Redirect : 302와 기능은 같으나 리다이렉트 시 요청 메소드와 본문 유지
  • 303 See Other : 302와 기능은 같으나 리다이렉트 시 요청 메소드가 GET으로 변하는걸 확실하게 보장

3. 특수 리다이렉션

  • 304 Not modified : 캐시를 목적으로 사용. 클라이언트가 요청한 리소스가 수정되지 않았음을 알려주는 기능. 따라서 클라이언트는 로컬PC에 저장된 캐시를 재사용할 수 있음.

참고로 영구 리다이렉션과 일시 리다이렉션은 육안으로 보기엔 비슷함. 그러나 브라우저 입장에서는 이 차이가 뚜렷하다. /members리소스가 /users로 영구적으로 바뀌었다고 하면 브라우저는 그 변화를 반영한다. 즉 영구 리다이렉션은 브라우저가 그 변화를 반영함그러나 일시 리다이렉션의 경우는 일시적으로 리소스 위치가 바뀐 거니까 브라우저가 그 변화를 반영하지 않는다. 즉 클라이언트 입장에선 일시 리다이렉션의 경우 향후 보내는 요청도 바뀐 URI가 아니라 기존 URI로 보내야 한다. 영구 리다이렉트를 서버가 하는 리다이렉트, 일시 리다이렉트를 클라이언트가 하는 리다이렉트라고도 부른다.

 

4XX - Client Error

: 클라이언트 측의 request에 잘못된 문법 등으로 서버가 요청을 수행할 수 없는 것. 오류의 원인이 클라이언트 쪽에 있음을 의미한다. 

  • 400 Bad Request : 요청 구문, 메시지 등등 오류. 요청 파라미터가 잘못 되거나 API 스펙이 맞지 않는 상황 등..클라이언트는 요청 내용을 다시 검토하고 보내야 한다.
  • 401 Unauthorized : 클라이언트가 해당 리소스에 대한 인증이 필요함을 의미. 인증(Authentication)되지 않음을 의미. 따라서 원래 이름은 Unauthentiaction이 맞는 듯..ㅠ
  • 403 Forbidden : 서버가 요청을 이해했지만 승인을 거부. 주로 인증은 됐는데 권한이 없는 상황일 때 이런 응답 코드를 받음
  • 404 Not Found : 요청 리소스를 찾을 수 없을 때. 요청 리소스가 서버에 없을 때 등등

 

5XX - Server Error

: 서버 문제로 오류가 발생한 것

  • 500 Internal Server Error : 서버 내부 문제로 오류가 발생함을 의미. 애매하면 걍 다 500 오류임
  • 503 Service Unavailable : 서버가 일시적 과부하 또는 예정된 작업으로 잠시 요청을 처리할 수 없을 때

 

4XX 에러는 클라이언트 쪽 문제니까 다시 시도해도 똑같이 안 되지만, 5XX에러는 서버 측 문제라 다시 하면 될 수도 있다는 차이점이 있다.

 

 

 

의존성 주입(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에서 한 번만 수정하면 된다. 

+ Recent posts