(React) 4. 복습

김동우·2021년 7월 30일
1

wecode

목록 보기
21/32
post-thumbnail

잠깐! 시작하기 전에

이 글은 wecode에서 실제 공부하고(이제 사전 스터디는 아닙니다.), 이해한 내용들을 적는 글입니다. 글의 표현과는 달리 어쩌면 실무와는 전혀 상관이 없는 글일 수 있습니다.

또한 해당 글은 다양한 자료들과 작성자 지식이 합성된 글입니다. 따라서 원문의 포스팅들이 틀린 정보이거나, 해당 개념에 대한 작성자의 이해가 부족할 수 있습니다.

설명하듯 적는게 습관이라 권위자 발톱만큼의 향기가 날 수 있으나, 엄연히 학생입니다. 따라서 하나의 참고자료로 활용하시길 바랍니다.

글의 내용과 다른 정보나 견해의 차이가 있을 수 있습니다.
이럴 때, 해당 부분을 언급하셔서 제가 더 공부할 수 있는 기회를 제공해주시면 감사할 것 같습니다.


서론

이번 포스팅은 React로 뭔가 만들어본 시점에서 놓친 개념이 있는지 생각해보는 글입니다.

오래 생각한 주제일수록 길수도 있고, 아닐수록 상당히 짧을 수 있습니다.

시작해보겠습니다.

React

React는 표면적으로 말하자면 하나의 라이브러리입니다.

웹의 발전에 따라 우리는 이제 웹 애플리케이션이라는 말을 쓸 정도로 방대한 데이터와 페이지, 그리고 더 많은 무언가를 구현해야 할 필요가 있습니다.

이를 위해 페이스북에서는 가상 DOM 환경을 활용한 성능개선 프로젝트를 진행했습니다.

결과는 대성공이었고, 현재 React는 여전히 인기 많은 라이브러리로 자리잡았습니다.

사람들은 React의 무엇에 열광했을까요?

혁신

Angular와 Vue를 써본 사람이 아닌 제가 생각했을 때, 기존과는 다른 혁신적인 생각이 있었기 때문은 아닐까 싶습니다.

앞서 말한 가상 DOM을 활용했기에 React는 기존 프레임워크에서 요구하는 관리사항을 대폭 감소시켰습니다.

대표적으로 Model의 배제가 그 예시라고 합니다.

사용한 적 없는 프레임워크 관련 내용에서 제 생각은 최대한 배제하겠습니다.

React는 변화가 발생한 Component의 위치를 정확히 알고, 변경점을 매우 빠르게 찾아내서 View를 다시 구성합니다.

다른 프레임워크는 Model의 변경을 통해 View를 바꿔주는 구조인데, React는 Model을 변경하는게 아니라 View만을 생각하는 구조로 설계되었습니다.

이런 근본적인 생각의 전환이 React를 조금 더 높은 위치에 올려주지 않았을까 합니다.

분할

아시다시피, React는 컴포넌트에 매우 진심입니다.

모든것은 컴포넌트로 시작하는데다가 대부분 컴포넌트 내부에서 사용자의 작업이 이루어집니다.

어쩌면 하루종일 특정 계층에 머무르며 해당 계층의 컴포넌트를 돌아다녀야만 하는 일도 있죠.

하루면 다행이라는 생각이 들지도 모르겠지만, 아직 저는 하루도 긴 시간이라고 생각합니다.

이런 컴포넌트들은 사람들 저마다의 기준에 따라 다르고, 심지어 같은 팀원끼리 정해둔 규칙으로도 기준을 통일시킬 수 없습니다.

그만큼 자유도가 높은 개념인데다 단순하지 않은 작업이 컴포넌트의 분할이라고 생각합니다.

그러나 잘 나뉜 컴포넌트들을 보면 초보자인 제가 봐도 유지보수에 용이하겠다는 생각이 듭니다.

저 밑 어딘가에 깔린 분할에 의한 렌더링 성능개선 또한 하나의 이유가 될테고요.

이러한 장점들이 React를 성장시킨 요소가 아닐까 생각합니다.

그렇다면 왜 분할이 성능을 개선시켰고, 유지보수에 용이한지 이전글과 비슷한 맥락으로 얕게 다시 한 번 짚어봅시다.

Virtual DOM, render()?

늘 그렇듯 오늘도, 내일도 React는 가상 DOM을 이용합니다.

가상 DOM의 장점은 너무나도 많은 글이 있으니까 저는 개념에 대해 생각하겠습니다.

먼저, 가상 DOM, 그럼 당연히 실제 DOM도 존재합니다.

당연한 말입니다.

그런데 왜 둘을 구분했을 때, 더 나은 성능의 웹 앱을 만드는 것이 가능해질까요?

쉽게 볼 수 있는 render()가 모든 컴포넌트에 존재하는 이유, 렌더링의 개선입니다.

React는 변경된 DOM 노드 위치를 정확히 파악해서 가상 DOM 에 먼저 반영하고, 변경사항을 실제 DOM과 동기화하는 과정을 거칩니다.

이 비교가 바로 성능개선에 제대로 기여하고 있는 것이죠.

React는 변경된 컴포넌트만을 다시 동기화한다.

이게 바로 React의 핵심, 재조정입니다.

어떻게 트리 탐색 알고리즘을 최적화했는지도 나오니 한 번은 꼭 보는게 좋다고 생각합니다.

그렇다고 실제 DOM에 비해 가상 DOM이 메모리를 덜 차지한다던가 하는 점에서 효율적인 성능 개선이 이루어지는 것은 아닙니다.

React는 가상 DOM과 실제 DOM 둘 다 가지고 있는 것이며, SPA가 아닌 MPA 방식으로 다수의 페이지를 구현할 경우 사실상 실제 DOM 만을 사용하는 바닐라에 비해 메모리 효율은 떨어질 수 밖에 없습니다.

단, React는 새로고침과 페이지간 이동에 있어 뛰어난 효율을 보일 수 있도록 설계되었기 때문에 SPA 방식의 웹 앱에서는 바닐라에 비해 더 나은 UX를 제공할 수 있습니다.

가상 DOM - 실제 DOM의 비교는 다음 사항을 이해하면 됩니다.

  1. 두 DOM 트리를 비교해서 변경점이 있는 부분만을 실제 DOM, View에 반영한다.

  2. 비교에서 다른 엘리먼트 타입을 가질 경우 변경, 다른 트리 구조로 판단한다.

  3. key props를 엘리먼트 비교, 판단에 활용한다.

이를 통해 도출된 변경사항은 해당 컴포넌트들의 render() 호출로 이어집니다.

CRA

이렇게 좋은 React를 사용하기 위한 가장 쉬운 방법 중 하나는 페이스북이 제공하는 create-react-app toolchain을 활용하는 것입니다.

무료로 배포된데다, 실제 페이스북 CRA team의 github는 언제나 문전성시를 이루고 있습니다.

CRA를 사용하면 하나의 리액트 프로젝트를 만들 수 있는 기본적인 환경이 완성됩니다.

@babel, webPack 등을 하나의 명령어로 아주 쉽게 제공받습니다.

이런 환경을 제공해주기에 React가 성장할 수 있었지 않을까 다시금 생각이 드네요.

Component

컴포넌트는 UI의 단위로, 기능을 내포하고 있고, 재사용성이 있으며 유지보수에 용이하도록 충분히 직관적인 하나의 인스턴스, 혹은 클래스, 혹은 함수입니다.

내부에는 메소드가 있을수도, 상태가 존재할수도, 외부로부터 값을 상속받을수도, 무언가를 반환할수도 있는 그런 무언가입니다.

Js식 표현으로 객체라고 생각할수도 있겠습니다.

모호한 기준이지만 전에도 생각해봤듯,

  1. 재사용성이 있어야 한다.

  2. 가독성을 위해 분리할수도 있다.

두 기준을 생각하며 분리하려 노력하고 있습니다.

JSX

Js + HTML 라는 표현을 사용하는 React의 확장 문법입니다.

실제 render() 메소드 내에 작성할 때 사용하는 문법으로, Javascript Syntax Extension의 약어입니다.

예시는 다음과 같습니다.


render() {
  const { handleSubmit, setCommentFormState } = this;

  return (
    <form className={`${styles.commentsForm}`} onSubmit={handleSubmit}>
      <PhotoCommentInput setCommentFormState={setCommentFormState} />
      <i className="far fa-smile-wink"></i>
      <button>게시</button>
    </form>
  );
}

실제 제가 구현한 코드의 일부입니다.

Router

React는 단일 페이지로 애플리케이션을 배포하게 해줍니다.

이러한 방식을 SPA 라고 부르며, 앞서 말했듯 리소스의 낭비를 줄일 수 있는 하나의 방식으로 급부상했습니다.

SPA 방식을 유지할 수 있도록 각 페이지의 메인 트리를 구성하는 컴포넌트들을 React Router를 통해 결합할 수 있습니다.

이를 통해 각 컴포넌트의 연결을 하나의 네트워크와 같은 구조로 생각할 수 있고, URL을 활용해 원하는 컴포넌트를 연결시킬 수 있습니다.

이러한 기능을 CRA toolChain에서 기본적으로 제공하는 것은 아니고, 3rd-party library인 react-router-dom을 사용해서 구현해야 합니다.

BrowserRouter, Routes, Switch

rrd(react-router-dom) 를 통해 우리는 하나의 망 연결과 같은 효과를 낼 수 있습니다.

rrd에 내장된 BrowserRouter(a.k.a Router), Routes, Switch 등의 이름을 가지는 컴포넌트들은 일반적으로 Routes.js 파일 내에서 관리되며, 해당 컴포넌트들은 각각의 기능을 가지고 있습니다.

BrowserRouter

as Router 이름으로 주로 사용하는 컴포넌트입니다.

해당 컴포넌트는 최상위 컴포넌트로, r-r-d를 통한 망 구축에서 필수적인 컴포넌트입니다.

html5 history API 를 활용한 UI 업데이트를 가능하게 하는 컴포넌트입니다.

해당 컴포넌트는 HashRouter와는 달리 동적인 방식으로 페이지를 구성하는데 사용합니다.

Routes

path를 입력받아 해당 도메인에 맞는 컴포넌트를 연결시켜주는 컴포넌트입니다.

표현에 따라 다르겠지만, 할당받은 URL에 해당하는 컴포넌트를 그려주는 역할을 한다고 생각할 수 있습니다.

Switch

Switch 컴포넌트는 Routes 컴포넌트들을 제어하는 컴포넌트입니다.

통상적으로은 <Routes exac path = "" />의 형태로 배치하지만, 만약 path만 작성하거나, 혹은 해당하는 URL이 없을 때를 대비할 수 있는 하나의 방법이 됩니다.

Switch 컴포넌트의 경우 해당 컴포넌트로 감싸진 Routes 컴포넌트들을 수직으로 확인하며 현재 path와 일치하는 단 하나의 Routes만을 render 합니다.

그렇기에 전부 일치하지 않는 상황에서의 예외 페이지를 구성해두고 render할 수 있도록 할 수 있습니다.

history, match, location

r-r-d 라이브러리를 사용하면, 연결된 컴포넌트에는 3가지 값이 props로 존재하게 됩니다.

해당 데이터들이 어떤 의미인지 생각해봅시다.

history

history의 경우 현재 컴포넌트 이전에 어떤 컴포넌트들을 거쳤는지 알 수 있게 해주는 url 정보를 가지고 있습니다.

r-r-d 를 사용하면 해당 url path에 맞는 컴포넌트를 반환하니, url 정보는 곧 컴포넌트를 의미한다고 생각하겠습니다.

history는 이후 다른 주제인 Link, withRouterHOC 에서 더 확인합시다.

match

match의 경우 말 그대로 일치한 정보입니다.

우리가 <Route /> 컴포넌트에 할당한 path값과 현재 url이 어떻게 매치했는지에 대한 정보를 담고 있습니다.

현재까지는 크게 쓸 일 없었으니 넘어가겠습니다.

location

location은 바닐라에서의 location과 비슷한가 생각할 수 있겠습니다.

현재 url의 정보를 가지고 있는데, 이를 활용하면 다양한 작업을 할 수 있지 않을까 생각합니다.

또한 search()를 통해 현재 url의 queryString 값을 가져올 수 있습니다.

드디어 메인 주제를 맞이했습니다.

실제로 우리가 다루는 각 컴포넌트(UI)들의 다양한 event 처리에서는 페이지의 이동을 구현하는 경우가 많습니다.

이 때 사용할 수 있는 대표적인 방법 2가지입니다.

Link는 마치 <a> tag와 비슷하게 동작하는 것을 구현한 컴포넌트입니다.

실제로 r-r-d 를 사용하는 상태에서 <a> tag를 잔뜩 사용하면 Link를 사용하라는 경고가 나오는데, Link를 사용하면 <a> tag에 비해 React 스러운 이동이 가능해집니다.

Link 는 단순하게 현재 url의 값을 변경시키기만 하는 컴포넌트가 아닙니다.

당장 자주 사용하는 <Link to = /> 의 경우, to에 string, object, function 3가지 데이터를 할당할 수 있는데, 이는 페이지 이동간 state 전달이 자유로워지는 효과가 있습니다.

현재는 공식문서의 내용 중 일부만 사용했는데, 앞으로 자주 사용할 컴포넌트인 것 같습니다.

withRouterHOC

withRouterHOC는 <Routes /> 컴포넌트로 싸여있지 않는 컴포넌트에 대해 라우팅 기능을 사용할 수 있게 해주는 일종의 상위 컴포넌트입니다.

라우팅 기능을 사용할 수 있다는 말은 곧, history, match, location 에 해당하는 데이터를 props로 가질 수 있게 된다는 말이 됩니다.

이에 form 태그에서 특정 사이트로 이동시키는 기능을 위해 withRouter로 해당 컴포넌트를 감싼 뒤 export 해주는 등의 예시가 있겠습니다.

import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import LoginInput from './LoginInput/LoginInput';
import styles from './LoginForm.module.scss';

class LoginForm extends React.Component {
	
  handleSubmit = e => {
    const { loginId, loginPw } = this.state;
    const { history } = this.props;
    e.preventDefault();
    loginId.includes(`@`) && loginPw.length > 8
      ? history.push(`/main-dongwu`, this.state)
    // Routes 컴포넌트 없이도 history 사용 가능
      : alert('ID, Password를 확인하세요.');
  }
  ...

  render() {
    const { handleSubmit, setParentState } = this;
    const { disabled } = this.state;
    return (
      <>
        <form
          className={`${styles.loginBlockForm}`}
          onSubmit={handleSubmit}
          action=""
        >
       	...
        </form>
      </>
    );
  }
}

export default withRouter(LoginForm);
// 이렇게 감싸서 export할 경우 라우팅 기능을 사용할 수 있습니다.

이렇게 해서 r-r-d의 전반적인 내용을 살펴보았습니다.

다음은 sass입니다.

SASS

소규모의 웹 애플리케이션을 만들 때만 해도 가끔 css의 문법 기능이 부실하다거나, 관리가 어렵다는 생각을 하게 됩니다.

이를 해결하기 위해 등장한 것이 바로 sass, node-module입니다.

이것 또한 하나의 3rd-party-library인데, css가 답답하고, 유지보수가 어려우신 분들은 한 번 시도해보시는 것을 추천합니다.

sass의 경우 다양한 기능을 지원합니다.

모든 기능을 사용해본 것은 아니지만, 그래도 나름 3~4개 기능에 대해 익숙해지려 노력했고, 여기에 적어보겠습니다.

Nested

sass의 경우 html 레이어 계층 구조를 nested 구조로 표현할 수 있게 합니다.

이를 통해 우리는 보다 쉽게 구조를 파악하고, 어느 위치에 어떤 style 속성을 적용할지 직관적으로 판단할 수 있습니다.

.loginBlockLabel {
  position: relative;
  margin: 0 40px 8px 40px;
  border: 1px solid var(--grey);
  border-radius: 4px;
  background-color: var(--light-grey);
  color: var(--input-font-color);

  > label {
    position: relative;

    > input {
      padding: 9px 0 7px 8px;
      border: none;
      background-color: var(--light-grey);
      outline: none;
    }
  }
}

위 코드는 실제 제 LoginInput 컴포넌트의 scss 파일 코드입니다.

대충 봐도 4개의 레이어로 이루어졌구나, 생각할 수 있습니다.

직관적인 스타일 시트를 작성할 수 있게 해주는 기능이 바로 중첩, nested 기능입니다.

@mixin - @include

그냥 이렇게 nested만 지원해도 만족스럽지만, 더 다양한 기능이 있습니다.

바로 @mixin - @include 를 통한 스타일을 함수와 같이 적용할 수 있게 됩니다.

mixin은 스타일의 추상화, include는 호출이라고 생각하면 됩니다.

아래는 예시입니다.

@mixin flex-set(
  $flex-direction: column,
  $justify-content: center,
  $align-items: center
) {
  display: flex;
  flex-direction: $flex-direction;
  justify-content: $justify-content;
  align-items: $align-items;
}

@mixin size($width: 0px, $height: 0px) {
  width: $width;
  height: $height;
}

.loginBlockLabel {
  @include size(268px, 40px);
  position: relative;
  margin: 0 40px 8px 40px;
  border: 1px solid var(--grey);
  border-radius: 4px;
  background-color: var(--light-grey);
  color: var(--input-font-color);

  > label {
    @include flex-set(column, center, center);
    position: relative;

    > input {
      @include size(258px, 36px);
      padding: 9px 0 7px 8px;
      border: none;
      background-color: var(--light-grey);
      outline: none;
    }
  }
}

이런 방식으로 중복되는 속성들을 정리할 수 있습니다.

Nested, mixin-include 둘 다 유지보수의 관점에서 상당히 매력적인 기능입니다.

module

가끔 특정 사이트들을 devTools로 관찰하면 class 명이 뭔가 해싱된건가? 하는 생각이 들 때 있습니다.

맞습니다. sass의 module 기능을 활용하면 모든 클래스명은 해당 컴포넌트만의 독자적인 클래스명으로 바뀝니다.

이것은 scss 파일의 확장자를 변경해주기만 하면 제공되는 기능인데, 단 JSX 내부에서의 적용이 약간 번거롭습니다.

sass 모듈을 사용했을 때, 우리가 아는 그 모듈의 이점을 다 가지게 됩니다.

module화 된 scss 파일들은 클래스끼리 간섭을 전부 차단하고, :root로 작성되는 것이 아닌 이상 다른 컴포넌트와 스타일 중복이 발생하지 않습니다.

이는 유지보수의 관점에서 매우 편리한데, 흔히 발생하는 CSS 터짐 현상을 해결하는 가장 효과적인 방법 중 하나입니다.

다음은 코드 예시입니다.

import React from 'react';
import styles from './LoginInput.module.scss';
// scss 파일명을 확인해보세요.

class LoginInput extends React.Component {
  
  ...
  renderInput = (labelId, inputId, inputType, placeholder) => {
    const { loginValidation } = this;
    const { setParentState } = this.props;
    return (
      <div className={`${styles.loginBlockLabel}`}>
      // 클래스명을 확인해보세요
        <label id={labelId}>
          <input
            id={inputId}
            type={inputType}
            placeholder={placeholder}
            onChange={event => {
              loginValidation();
              setParentState(labelId, event.target.value);
            }}
          />
        </label>
      </div>
    );
  };
  ...

}

export default LoginInput;

import - className 둘만 변경해줘도 module.scss는 정상적으로 작동합니다.

State-Props

해당 내용은 포스팅에서 다루지 않겠습니다.

다만, 제가 공부했던 링크를 첨부하겠습니다.

공식문서 - 컴포넌트, State

위 링크의 글 내부 링크를 흘러가듯 보시면 대부분의 개념을 이해할 수 있습니다.

그러나 아직 저도 미숙한 부분이 많기 때문에 확실하지 않은 생각을 적지는 않겠습니다.

Mock-Data

백엔드 API가 없을 때, 프론트엔드 개발을 할 수 없는 것은 아닙니다.

해당 DB 자료와 비슷한 구조의 가짜 데이터를 생성해서 설계를 이어나갈 수 있습니다.

mockData는 가짜 데이터를 의미합니다.

Constant

상수형 mockData의 경우, 정적인 컴포넌트에 적용할 수 있는 데이터입니다.

예를 들면, footer, nav 등에 위치한 <li> 태그들의 내용이 될 수 있겠습니다.

처음에는 상수형 데이터들의 경우 유지보수를 위한 데이터 분리인가? 하는 생각이 듭니다.

데이터의 분리의 힘은 생각보다 가시적으로 느껴지긴 합니다.

사용 후 제 생각에도 컴포넌트 내에서 해당 데이터를 하드코딩하는 것은 피할 수 있다면 최대한 피하는 것이 맞는것 같습니다.

코드는 상수형 mock data의 예시로 올려보겠습니다.


const FOOTER_LIST = [];
const FOOTER_LIST_TEXT = [
  `소개`,
  `블로그`,
  `채용 정보`,
  `도움말`,
  `API`,
  `개인정보처리방침`,
  `약관`,
  `인기 계정`,
  `해시태그`,
  `위치`,
];

FOOTER_LIST_TEXT.forEach((elem, idx) => {
  let FOOTER_LIST_INFO = { id: idx + 1, href: `#`, text: elem };
  FOOTER_LIST.push(FOOTER_LIST_INFO);
});

export default FOOTER_LIST;

Json

Json 형태의 Mock data의 경우 fetch() 메서드를 활용할 수 있도록 설계되었습니다.

백엔드의 완성도와 무관하게 프론트단에서 임의로 Json 파일을 만들어 로직을 설계할 수 있게 해주는 친구입니다.

확실히 대규모 정보를 다루게되는 현 시점에서는 충분히 훌륭한 타개책이 될 수 있다고 생각합니다.


[
  {
    "id": 1,
    "feedProfileImg": "/images/Dongwu/Main/freeImage.png",
    "feedImg": "/images/Dongwu/Main/freeImage.png",
    "feedId": "steam_udon1",
    "feedText": "가나다라마바사"
  },
  {
    "id": 2,
    "feedProfileImg": "/images/Dongwu/Main/freeImage.png",
    "feedImg": "/images/Dongwu/Main/freeImage.png",
    "feedId": "1nodu_maets",
    "feedText": "사바마라다나가"
  },
  {
    "id": 3,
    "feedProfileImg": "/images/Dongwu/Main/freeImage.png",
    "feedImg": "/images/Dongwu/Main/freeImage.png",
    "feedId": "steam_udon2",
    "feedText": "아자차카타파하"
  }
]

실제 DB에서는 snake case를 활용하지만, 저의 데이터는 손에 익은 camel case입니다.

이후 프로젝트에서 생성할 mock data 파일은 꼭 snake로 적어봐야겠네요.

마치며

이상으로 그동안 리액트를 나름 열심히 다뤄왔던 보람이 있는 것 같습니다.

이제는 더 깊고 바닥에 가까운 지식들을 모아가는 것과 동시에 넓은 범용성을 가지는 코드를 만들어볼까 합니다.

현실은 state-props를 어떻게 설계할지부터 꼬이지만, 그래도 내일의 저는 오늘보다 낫지 않을까요.

긴 글 읽어주셔서 감사합니다.

그럼 이번 글은 여기서 마치도록 하겠습니다.

0개의 댓글