Frontend/React

React - 1. 리액트 입문 정리

둉이 2021. 11. 1. 03:20

리액트의 특징

- state 값이 변경되면 DOM을 업데이트하지 않고 아예 새로 만듦
- JSX라는 문법을 사용
- Webpack, babel 등


리액트의 Virtual DOM

- 브라우저에 실제로 보여지는 DOM이 아니라 메모리에 가상으로 존재하는 DOM
- Javascript 객체
=> 실제 DOM보다 속도가 굉장히 빠름


JSX

- babel을 통해 JSX 문법이 Javascript로 변환
- 닫힌 태그가 꼭 있어야 함(혹은 self closing tag)
- 하나의 최상단 태그로 감싸야 함
=> 리액트의 Fragment 사용 가능(<></>)
- JSX 내부에 자바스크립트를 사용해야 할 경우 {}로 감싸서 사용


Babel

- 자바스크립트 문법 확장 도구
- 아직 지원되지 않는 최신 문법 혹은 실험적인 문법을 정식 자바스크립트 형태로 변환하여 구형 브라우저같은 환경에서도 실행이 가능하도록 해주는 도구


JSX vs HTML 차이점

- 컴포넌트에 style 속성을 전달할 경우에는 객체 형태로 전달해야 함 + camel case
- class가 아닌 className 사용


props

- JSX 컴포넌트에 전달되는 속성

- 값 전달을 생략할 경우에는 true가 전달됨
- 컴포넌트명.defaultProps = { ... } 으로 props의 기본값 설정 가능

Hello.defaultProps = {
   name: 'babo',
}

 

Q. 컴포넌트 태그 안에 다른 컴포넌트를 넣었는데 안보여요!
A. 자식 컴포넌트들이 props.children으로 전달되어 그렇다. 부모 컴포넌트에서 children props를 받아서 JSX 컴포넌트 안에 넣어주자.

/* App.js */
const App = () => {
  return (
    <Wrapper>
      <Hello name="배대시"></Hello>  // Wrapper 안에 있는 Hello 컴포넌트가 페이지에 보이지 않음
      <Hello></Hello>
    </Wrapper>
  )
}

/* Wrapper.js */
const Wrapper = ({ children }) => {
  const style = {
    border: '2px solid black',
    padding: '16px',
  };
  return (
    <div style={style}>
      {children}
    </div>
  )
}

 

propTypes

- 부모로부터 전달받은 props의 type을 검사하는 모듈

- 자식 컴포넌트에서 명시한 props의 데이터 타입과 부모로부터 넘겨받은 props의 데이터 타입이 일치하지 않으면 콘솔 에러 출력

이런 식으로 Warning 출력

 

- 사용법

import PropTypes from 'prop-types'

/* Hello.js */
Hello.propTypes = {
  name: PropTypes.string  // name이 string 타입이 아니면 에러 발생
}

 

 

cf) 리액트 코드 자동완성

: vscode에서 Reactjs code snippets 플러그인 설치

종류 설명
rcc 클래스 컴포넌트 생성
rrc 클래스 컴포넌트와 react-redux를 연결하여 생성
rcjc import와 export 없이 클래스 컴포넌트 생성
rwwd import 없이 클래스 컴포넌트 생성
rsc 화살표 함수형 컴포넌트 생성
rsf function 키워드의 함수형 컴포넌트 생성

 

조건부 렌더링

- 삼항 연산자 혹은 &&을 이용하여 조건부 렌더링 가능

 

Hook API

1. useState

- 아래 형태로 state와 state를 변경하는 함수를 선언하여 사용

const [number, setNumber] = useState(0)

 

- setState 함수는 이전 state 값을 파라미터로 받음

  const [number, setNumber] = useState(0)
  const plus = () => setNumber(prev => prev + 1)
  const minus = () => setNumber(prev => prev - 1)

  return (
    <div className="App">
      {number}
      <button type="button" onClick={minus}>-</button>
      <button type="button" onClick={plus}>+</button>
    </div>
  )

 

- state 객체 하나로 여러 개의 state를 한 번에 관리할 수도 있음

  : 객체 형태로 state 선언, 스프레드 연산자(...)를 이용하여 setState에 값 전달

const App = () => {
  const [input, setInput] = useState({
    name: '',
    nickname: '',
  })
  const { name, nickname } = input
  const change = ({ target }) => {
    setInput({
      ...input,
      [name]: target.value,
    })
  }
  const reset = () => setInput({
    name: '',
    nickname: '',
  })
  return (
    <div className="App">
      {name}({nickname})
      <input name="name" type="text" placeholder="이름" onInput={change} value={name}></input>
      <input name="nickname" type="text" placeholder="닉네임" onInput={change} value={nickname}></input>
      <button type="button" onClick={reset}></button>
    </div>
  )
}

 

- 배열 state에 새로운 값을 추가할 때는 기존 state의 값을 변경하면 안됨

: 기존 배열을 복사하여 새로운 배열을 만든 후, 새로운 배열에 수정 사항을 반영해야 함

=> 스프레드 연산자(...) 혹은 concat을 사용

const [lst, setLst] = useState([1, 2, 3])

// 1. 스프레드 연산자 사용
setLst([ ...lst, 4 ])
// 2. concat 사용
setLst(lst.concat(4))

 

- 배열 state에서의 값 삭제는 filter() 고차함수를 활용

- 배열 state에서의 값 수정은 map() 고차함수를 활용

 

2. useRef

- DOM에 직접 접근해야 할 경우에 사용

- useRef()를 사용하여 ref 객체를 만들고, 접근하려는 컴포넌트의 ref 값으로 전달

- current 객체를 통해 지정한 DOM에 접근 가능

const App = () => {
  const nameRef = useRef()
  const reset = () => nameRef.current.focus()

  return (
    <div className="App">
      {name}({nickname})
      <input ref={nameRef} name="name" type="text" placeholder="이름"></input>
      <input name="nickname" type="text" placeholder="닉네임"></input>
      <button type="button" onClick={reset}></button>
    </div>
  )
}

 

- 컴포넌트 내의 다음과 같은 변수들을 useRef()를 사용하여 관리할 수 있음

  1. setInterval, setTimeout으로 생성된 ID
  2. 외부 라이브러리를 통해 생성된 인스턴스
  3. 스크롤 위치
  const nextId = useRef(4)
  const onCreate = () => {
    nextId.current += 1  // 변수명.current로 해당 값에 접근할 수 있음
  }

=> useRef()를 이용하여 변수를 관리하면 리렌더링 없이 변수 값을 변경할 수 있음!

 

3. useEffect

- 컴포넌트 마운트/언마운트시 혹은 props의 업데이트시에 특정 작업을 실행되도록 함

- 첫 번째 파라미터는 함수, 두 번째 파라미터는 해당 함수의 의존값 배열을 전달

- return되는 함수는 cleanup의 역할

 

- 빈 배열 전달

: 해당 컴포넌트가 렌더링/사라질 때 실행(마운트/언마운트 시)

useEffect(() => {
	console.log('컴포넌트가 처음 렌더링될 때 실행')  // componentDidmount
    return () => {
    	console.log('컴포넌트가 사라질 때 실행')  // componentWillUnmount
    }
}, [])

=> 주로 다음과 같은 상황에 사용됨

  • 마운트 사용 예시
  1. props 값을 컴포넌트의 state로 설정
  2. 외부 API 요청
  3. 라이브러리 사용(D3.js, Video.js 등)
  4. setInterval 혹은 setTimeout을 이용한 반복 작업
  • 언마운트 사용 예시
  1. clearInterval, clearTimeout
  2. 라이브러리 인스턴스 제거

 

- 배열에 특정 값(props)를 전달하는 경우

: 배열 내의 값들이 변경될 때마다 실행됨

useEffect(() => {
	console.log('값이 바뀐 후 혹은 처음 설정될 때 실행')  // componentDidmount + componentDidUpdate
    return () => {
    	console.log('값이 바뀌기 직전에 실행')  // componentWillUnmount
    }
}, [name, email])

 

- 배열 자체를 생략하는 경우(파라미터를 전달하지 않는 경우)

: 컴포넌트가 리렌더링될 때마다 실행됨 (자식 컴포넌트의 경우에는 부모 컴포넌트가 리렌더링될 때에도 실행됨)

useEffect(() => {
	console.log('리렌더링될 때 실행')
})

 

4. useMemo

- 메모이제이션, 자주 연산되는 값을 저장하여 재사용

- 첫 번째 파라미터는 함수, 두 번째 파라미터는 해당 함수의 의존값 배열을 전달

- 두 번째 파라미터 배열의 값이 변경되지 않는다면 이전 값을 재사용하여 성능 최적화

const count = countFunc()  // 컴포넌트가 렌더링될 때마다 실행되므로 비효율적

const count = useMemo(() => {  // 값을 저장하여 사용하므로 효율적
	countFunc(users)
}, [count])

 

cf) React.memo

- props가 바뀌지 않았다면 컴포넌트의 리렌더링을 방지하는 성능 최적화 함수

- export할 때 컴포넌트 자체를 React.memo()로 감싸서 사용하면 됨

import React from 'react'

const UserList = ({ users, onRemove, onToggle }) => {
  return (
    <div>
      {users.map(user => (
        <User
          user={user}
          key={user.id}
          onRemove={onRemove}
          onToggle={onToggle}
        />
      ))}
    </div>
  )
}
export default React.memo(UserList)  // 이렇게 감싸주면 됨

 

- React.memo는 기본적으로 얕은 비교로 동작

: 두 번째 파라미터로 비교함수를 전달하여 해당 컴포넌트의 prevProps와 curProps를 비교하여 true를 반환하면 컴포넌트 재사용

const areEqual = (prev, next) => {
  return (
    prev.title === next.title &&
    prev.content === next.content
  );
}

export default React.memo(Component, areEqual);

 

- 무한 스크롤이나 더보기 버튼을 클릭하여 새로운 요소를 생성하는 경우처럼 기존 렌더링된 요소들이 계속 렌더링되는 것을 방지하기 위해 주로 사용

 

 

5. useCallback

- useMemo 기반으로 만들어짐

- useMemo는 값을 재사용하는 반면, useCallback은 함수를 재사용하는 데 사용

=> 컴포넌트가 리렌더링될 때마다 함수가 재생성되는 걸 방지

 

- 첫 번째 파라미터는 함수, 두 번째 파라미터는 해당 함수의 의존값 배열을 전달

=> 의존값 배열에는 함수 내에서 사용되는 state 혹은 props를 포함(ref는 포함 X)

// before
const onRemove = id => setUsers(users.filter(user => user.id !== id))

// after
const onRemove = useCallback(
    id => setUsers(users.filter(user => user.id !== id)), [users]
)

 

6. useReducer

- 상태관리의 방식 중 하나

- 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리할 수 있음

  => 상태 업데이트 로직을 컴포넌트 바깥(다른 파일)에 작성하여 import할 수 있음

 

- reducer란?

: 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해 주는 함수

  action은 업데이트를 위한 데이터를 가지며, 주로 type 값을 지닌 객체 형태로 사용

// 리듀서 예시
const stateReducer = (state, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// action 객체 예시
{ type: 'INCREMENT' }
{ type: 'DECREMENT' }
{
	type: 'CHANGE_INPUT',
    key: 'email',
   	value: 'wastfg6972@naver.com',
}

 

- dispatch 함수를 사용하여 액션을 발생시키고 리듀서를 실행

: const [state명, dispatch] = useReducer(reducer명, 초기값) 형태로 선언하여 사용

function Counter() {
  const [number, dispatch] = useReducer(stateReducer, 0)

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={() => dispatch(number, { type: 'INCREMENT'})}>+1</button>
      <button onClick={() => dispatch(number, { type: 'DECREMENT'})}>-1</button>
    </div>
  );
}

export default Counter;

 

cf) useReducer vs useState

: 상태가 단순한 값이거나(ex boolean) 하나인 경우에는 useState로 관리하는 게 편함

  하지만 상태값을 여러 개 관리해야 한다거나 구조가 복잡해지는 경우에는 useReducer가 적절

 

7. Context API(React.CreateContext, useContext)

- 부모 컴포넌트에서 하위 자식 컴포넌트로 props를 전달해줄 때 여러 개의 컴포넌트를 거쳐서 전달해야 하는 기존 방식의 불편함을 해결하기 위한 방식

- 프로젝트 안에서 전역적으로 사용할 수 있는 값을 선언하여 사용할 수 있음

  => state가 될 수도 있고 라이브러리의 인스턴스가 될 수도 있음, 혹은 DOM도 가능

- React.createContext() 함수를 사용하여 Context를 정의할 수 있으며, Context 안에 Provider라는 컴포넌트를 통해 Context의 값을 정할 수 있음

: Provider의 value에 값 또는 함수를 전달하여 전역적으로 사용 가능

// Context 선언 및 export
export const UserDispatch = React.createContext(null)

// 위 방식처럼 선언한 후, 아래 return부에서 선언한 Context의 Provider 컴포넌트로 감싸야 함
return (
  <UserDispatch.Provider value={dispatch}>
    <CreateUser
      username={username}
      email={email}
      onChange={onChange}
      onCreate={onCreate}
    />
    <UserList users={users} />
    <div>활성사용자 수 : {count}</div>
  </UserDispatch.Provider>
 )

// import하여 사용 가능
import { UserDispatch } from './App'

 

- useContext를 사용하여 정의된 Context를 조회하여 사용할 수 있음

import { UserDispatch } from './App'

const dispatch = useContext(UserDispatch)
// ...
function User({ user, onRemove, onToggle }) {
  return (
    <div>
      <b
        onClick={() => dispatch({ type: 'TOGGLE_USER', id: user.id})}  // 해당 방식처럼 사용 가능
      >
        {user.username}
      </b>
      &nbsp;
      <span>({user.email})</span>
      <button onClick={() => dispatch({ type: 'REMOVE_USER', id: user.id})}>삭제</button>
    </div>
  );
}

 

- React.createContext

const Store = React.createContext(defaultValue)

: context 객체를 만드는 함수로, ProviderConsumer 컴포넌트를 반환함

  만들어진 context를 사용하려면 해당 context의 Provider 컴포넌트로 

  defaultValue는 Provider와 Consumer에서 적절한 value값을 찾지 못한 경우의 기본값으로 사용됨

 

- Context.Provider

: 정의한 context를 하위 컴포넌트에게 전달하는 역할

  Provider 내부에 하위 Provider를 배치할 수 있으나, 그럴 경우에는 하위 Provider 값이 우선시됨

  Provider 하위에 context를 가진 컴포넌트들은 Provider의 value인 state의 값이 변할 때마다 전부 리렌더링됨

 

- Context.Consumer

: context 변화를 구독하는 컴포넌트

  context의 자식으로는 함수 또는 함수 컴포넌트를 가짐

  함수의 매개변수로는 가장 가까운 Provider의 value가 전달됨

  상위 Provider가 존재하지 않는 경우에는 createContext()에서 정의한 defaultValue가 전달됨

 

커스텀 Hooks 만들기

- 컴포넌트를 만들면서 반복되는 로직을 분리하여 커스텀 Hooks로 만들어 재사용이 가능하다.

- use + 키워드 형태의 이름을 갖는 함수를 만들어서 export하여 커스텀 Hooks로 사용하면 된다.

// useInputs.js
import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(form => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;

// App.js
const initInputs = {
  username: '',
  email: ''
}
const [{ username, email }, onChange, reset] = useInputs(initInputs)

 

Immer를 사용한 불변성 관리

- 리액트에서의 불변성을 편리하게 관리할 수 있는 라이브러리

- 기존 스프레드 연산자(혹은 map, concat, filter)를 이용하여 불변성을 관리하던 방식 대체

=> immer를 사용하여 코드량을 단축시킬 수는 있으나 오히려 길어기는 경우도 있으니 주의해서 사용하자!

- 기존 방식보다 성능은 평균적으로 느림

 

- 사용법

1. immer 라이브러리 설치

# npm i immer
// 혹은
# yarn add immer

 

2. import하여 사용

: 보통 produce라는 이름으로 불러음

  첫 번째 파라미터는 prevState, 두 번째 파라미터는 업데이트 함수를 넣어줌

import produce from 'immer'

const state = {
  number: 1,
  dontChangeMe: 2
}

const nextState = produce(state, draft => draft.number += 1)

 

cf) React Developer Tools

: 리액트 디버깅용 크롬 확장 프로그램

 

React Developer Tools

Adds React debugging tools to the Chrome Developer Tools. Created from revision c213030b4 on 10/25/2021.

chrome.google.com

 

클래스형 컴포넌트

- render() 메소드가 꼭 있어야 함

- props와 state에 접근하기 위해서는 this.props 형태로 앞에 this를 붙여서 접근해야 함

- state 변경은 this.setState()를 이용하면 됨

: setState() 함수에 콜백 함수를 넣을 수도 있음!

this.setState({ counter: this.state.counter - 1 })
// 혹은
this.setState(state => { counter: state.counter - 1 })  // 연속적으로 setState를 호출할 때 사용하면 좋음

 

- defaultProps는 분리해서 선언해도 되고, 아래처럼 static 키워드로 선언해도 됨

- 컴포넌트 내에 메소드 선언시, this를 bind 해줘야 함

: 메소드를 화살표 함수로 선언하면 bind를 하지 않아도 됨 but 정식 자바스크립트 문법은 아니므로 CRA 프로젝트 내에서만 사용 가능

import React, { Component } from 'react'

class Hello extends Component {
  constructor(props) {
    super(props)
    this.handleIncrease = this.handleIncrease.bind(this)
    //this.handleDecrease = this.handleDecrease.bind(this)  // 안해도 됨
    this.state = {
      counter: 0
    }
  }

  handleIncrease() {
    console.log('increase')
  }

  handleDecrease = () => {
    console.log('decrease')
  }
  static defaultProps = {
    name: '이름없음'
  }
  render() {
    const { color, name, isSpecial } = this.props
    return (
      <div>
        <h1>{this.state.counter}</h1>
        <button onClick={this.handleIncrease}>+1</button>
        <button onClick={this.handleDecrease}>-1</button>
      </div>
    )
  }
}

export default Hello

 

편리한 개발 도구 모음

Prettier

- 코드 포매터, 스타일러 도구

- 사용 방법

1. .prettierrc 파일을 만들어서 내용을 입력

{
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true
}

 

2. prettier 익스텐션 다운로드

 

3. ctrl + , 키를 눌러서 VSCode 환경설정을 연 뒤, Format On Save 검색 후 체크 + Default Formatter에서 Prettier 선택

 

ESLint

- 자바스크립트 문법 검사 도구

- CRA 프로젝트에서는 기본 적용이 되어 있음

- 기본 설정 규칙 자체를 라이브러리로 제공함

  • eslint-config-airbnb
  • eslint-config-google
  • eslint-config-standard

=> 적용 방법

1. 해당 config 라이브러리 설치

# npm i eslint-config-standard

 

2. package.json 파일을 열어서 eslintConfig 부분을 아래와 같이 수정

"eslintConfig": {
   "extends": [ "react-app", "standard" ]  // 설치한 config파일의 이름을 끝에 추가
},

 

3. (prettier와 함께 사용하는 경우에만 설정) eslint-config-prettier 라이브러리 설치 후 2번처럼 eslintConfig 부분 뒤에 prettier 추가

# npm i eslint-config-perttier

"eslintConfig": {
   "extends": [ "react-app", "standard", "prettier" ]  // 뒤에 prettier 추가
},

 

- 문법 규칙 비활성화 방법

: package.json의 "rules"에서 비활성화하고자 하는 규칙에 0 값으로 설정하면 됨

"eslintConfig": {
   "extends": [ "react-app", "standard", "prettier" ],  // 뒤에 prettier 추가
   "rules" : {
      "react/jsc-filename-extension" : 0,  // rules에서 비활성화하고자 하는 규칙에 0을 주면 비활성화
      "no-unused-vars": 1
   }
},

 

Snippet

- 자주 사용되는 코드에 대한 단축어를 만들어서 사용할 수 있는 IDE 자체 기능

- react snippet 등의 확장 플러그인을 마켓에서 다운받아 사용하거나 직접 스니펫을 만들어서 사용할 수 있음