패캠 React Project 실전활용, Hooks, Context, Testing

TonyHan·2021년 8월 20일
0
post-thumbnail

https://slides.com/woongjae/react2021
https://slides.com/woongjae/fds17th-11
https://slides.com/woongjae/fds17th-12
https://slides.com/woongjae/fds17th-13
https://slides.com/woongjae/redux2021#/2
https://slides.com/woongjae/reactts2021#/2
https://slides.com/woongjae/fds17th-14
https://react.vlpt.us/

React Project 실전

HOC(Higher Order Component)

https://ko.reactjs.org/docs/higher-order-components.html

리액트 컴포넌트 로직을 재사용가능하게 해주는 기술이다. 하지만 지금은 훅이 좋은게 나와서 잘 안쓰기도 한다.

결국 HOC는 컴포넌트를 인자로 받아서 새로운 컴포넌트를 리턴하는 함수이다.

컴포넌트를 받아서 새로운 컴포넌트를 내놓는 것이였다.

이런 HOC는 로직 재활용에 좋은 기술이었다. 실재 Redux에서 이것을 connect 함수를 쓰기도 했다. relay 라이브러리에도 Fragment Cintainer 함수가 있었다.

그런데 우리는 이미 HOC를 알고 있었다.

withRouter이라는 함수를 썼었는데 실행결과를 계속해서 넘겨받아서 새로운 컴포넌트에서 사용했었다.

  1. HOC는 Cross Cutting Concerns

횡단 관심사 : https://ko.wikipedia.org/wiki/%ED%9A%A1%EB%8B%A8_%EA%B4%80%EC%8B%AC%EC%82%AC

같은 일을 하는 시점을 묶어서 횡단 관심사라고 부른다. 예를 들어서 api를 쳐서 결과를 가져오는 것도 횡단 관심사라고 부를 수 있다.

login을 하는 시점과 행위는 계속 페이지 별로 동시에 일어난다.

  1. HOC에 들어가는 인자를 바꾸지 말라

origin component를 받아다가 여러 컴포넌트의 조합처럼 놓고 통채의 컴포넌트를 반환하는 방식을 composition이라고 부른다.

  1. 관계없는 props를 묶인 컴포넌트로 보낸다.

이전에 했던 withRouter()과 같이 이전의 props를 결과로 보내는 거을 이야기 한다.

  1. Composability를 최대화해라

  2. 쉬운 디버깅을 위해서 새로 만들어진 컴포넌트에는 HOC가 적용된 인자로 넣은 컴포넌트라는 것을 넣어주어라.

예를 들어 withRouter 함수도 C로 인자를 받아서 내부에서 또 처리한다.

render Method안에서 HOC 사용금지
인자로 들어가 static method는 복사되어야 한다.
ref는 pass through되지 않는다.

위와 같이 render 안에서 HOC를 하면 안된다.


물론 이렇게 staticMethod를 별도의 method로 분류할 수도 있다.

Controlled Component & Uncontrolled Component

npx create-react-app controlled-uncontrolled-example

어떻게 엘리먼트를 지배할 것인지에 대해서 알아보자

즉 컴포넌트가 엘리먼트를 상태로 관리하는 것을 이야기 한다.

이걸 App.js에 넣으니 위와 같이 나왔다.

콘솔에서는 onChange를 처리해주지 않았다고 에러를 띄어주었다.

그래서 onChange를 사용하고 입력시 change라는 함수가 실행되도록 하였다. 참고로 change함수는 event라는 인자를 받게 된다.

그런데 보면 입력은 안되고 위와같이 출력만 된다.

이것을 바꾸기 위해서 input의 상태가 state에 저장되는데 만약 input이 바뀌게 되면 setState를 호출해서 값을 바꾸는 것이다.

여기에 버튼을 넣어서 나중에 서버쪽으로 보낼 수도 있을 것이다.


이번에는 Uncontrolled component를 다루어보자

하면 위와 같이 뜬다. 현재 쓰는 것은 realDOM이기 때문에 값을 꺼내올 수 있다.

하지만 위의 방식은 지양해야한다. 처음에는 inputRef가 null이다. 이것을 벗어나기 위해서는 한번이라고 마운트 된 다음에 inputRef값을 바꾸어 주어야 한다.

componentDidMount가 호출된 것을 확인한 다음 click을 눌렀을 때의 값을 확인해보니 우리가 입력한 값이 나온 것을 확인할 수 있다.

결국에는 이게 무엇이 다르냐면 render가 되는 시점에 inputRef에 저장해 놓고 나중에 꺼내쓴다는 것을 이야기 한다.

만약에 매번 state를 변경되고 UI도 바뀌는 경우라면 이런 ref방식보다 controlled component를 쓰는게 좋을 것이다.

만약에 입력부분 클릭스 focuse 되게 하려면 Uncontrolled component방식이 보다 좋을 수 있다.

Hooks & Context

Basic Hooks

훅은 클래스 컴포넌트에서만 state와 라이플사이클 쓰던것을 function 컴포넌트에서 사용가능, 컴포넌트 재사용 가능이라는 점에서 큰 의미가 있다.

React 공식홈피 훅에 대한 설명을 읽어보자

npx create-react-app react-hooks-example

자바스크립트 분해할당
https://beomy.tistory.com/18

이걸 이제 함수 컴포넌트로 바꾸어보자

useState를 사용한 객체는 사실 배열이다. setCount가 하는 역활은 count를 바꾸고 Example2를 다시 실행하는 역활이 된다.

잘 작동한다.

이걸 또 객체로 바꾸어 주는 방법이 있다. 물론 setCount는 우리가 임의로 정함 함수명이니 setState로 정의해도 된다.

잘 작동한다.

setState에 들어오는 것을 기존 state값에 의존성을 바꾸고 싶다면 함수로 적어줄 수도 있다.

이렇게 쓰는 이유는 다른 훅들과의 dependency가 중요해진다. 그러면 state가 다른 훅들에 의존적이 되면 문제가 생길 수 있기 때문에 위와 같이 작성해주어서 이를 피하는 것이다.

그런데 왜 function component 에서 이걸 처리하려고 할까? 위와 같은 이유들을 들 수 있다.

마지막 줄에서 class는 this.state를 render 사이에 공유하고 function은 공유하지 않는다는 것이다. render 사이에서 바뀐 state를 정확하게 표현할때는 function이 좋다. 그 반대는 별로이다.

useEffect는 여러가지 일을 대체할 수 있다. 그중에서 위에 나온 라이프 사이클 훅들을 다룰 수 있다.

Example4를 Example1의 내용을 모두 복사해서 붙여넣자.

그리고 코드를 위와 같이 작성해보자.

하고 App.js 에 붙여넣자

잘 올라가는 것을 확인할 수 있다.

Example2에서 Example5.jsx를 만들고 함수형으로 componentDid 시리즈를 사용해보자

위와같이 로그를 보면 Mount, Update 모두에 작동하는 거을 확인할 수 있다.

그런데 이런 useEffect함수는 두번째로 React.DependencyList를 넣을 수 있다. 이건 배열인데 여기에 빈 베열을 넣으면 클릭시 아무것도 안뜬다.

useEffect에서 함수가 뜰 타이밍을 조절할때 DependencyList를 사용하게 된다.

만약 이 부분이 없다면 항상 render가 된 이후에는 useEffect를 사용해 주세요이고
만약 이 부분이 []로 되어 있다면 최초에만 실행이 된다는 의미이다.

useEffect는 여러개를 사용할 수 있는데(순차적으로 실행된다) 빈배열에 있는 조건에 맞을때만 실행된다.

위쪽 빈칸은 count가 있는데 이게 빈칸으로 들어가야 한다. 뭐 짜피 이건 처음에만 쓰는 거니 비워두고

아래쪽으로 count에 의존적으로 변화되도록 바꾸어주자. count가 변화되었을때면 useEffect가 사용되도록 하는 것이다.

componentWillUnmount의 역활을 할 수 있도록 바꾸어주자.

새로운 함수를 반환하도록 만들어 주어서 cleanup하게 만들어주자.

위와 같이 빈 배열을 반환하는 것은 처음에만 실행되고 안불리다가 Example5가 사라질때 return 된다. 그래서 저 위치를 componentWillUnmount의 역활을 한다고 생각할 수 있다.

다음번 useEffect함수가 실행되기 전에 미리 이전 Dependency값으로 return 부분에 있는 함수를 실행해주고 다음 값으로 넘어가게 된다.

https://rinae.dev/posts/a-complete-guide-to-useeffect-ko
자세한 useEffect 가이드는 위 사이트에서 확인하자

Custom Hooks

훅에 useSomething을 붙여서 사용한다.

처음 만들어볼 훅은 가로창 사이즈가 변경시 변경된 숫자를 받아오는 훅을 받아오자.

너비가 출력된 것을 확인할 수 있다.

useEffect를 사용하니 너비에 따라 값이 바뀌는 것을 확인할 수 있다.

그러다가 만약에 어딘가에서 이 EventListener을 안사용하게 되면 cleanup을 사용해야 한다.

그래서 위와같이 작성해주면 처음에 render 될때만 이벤트가 실행되게 될것이다.

이렇게 만든 로직을 훅은 나중에 다른 컴포넌트가 재사용가능한 나만의 훅이 된것이다.

useHasMounted는 마운티드 되었을때 알려줄 수 있는 HasMounted라고 하는 state를 알려주는 훅

withHasMounted라는 HOC를 이용해서 컴포넌트를 인자로 넣었을때 새로운 컴포넌트가 나오는데 인자로 넣어준 컴포넌트에 HasMounted라는 props가 들어가게 된다.

위와 같이 작성해주면 간단한 HasMounted라는 props를 넣어주는 HOC를 만들어줄 수 있다.

한다음 hasMounted를 받아서 로그를 찍어보자

했더니 위와같이 false -> true로 바뀌는 것을 확인할 수 있다. 이는 처음에 App이 false였다가 랜더직후에 true가 props로 바뀌면서 다시 render가 되어서 true가 찍히는 것이다.

HasMounted를 위와 같이 만들어주었다. 그래서 useHasMounted는 상태를 만들고 그것을 바꾸는 로직이 담기어 있고 withHasMounted는 만들어진 hasMounted를 props로 내려보내주는 것이다.

Additional Hooks

리액트 라이브러리에서 추가적으로 제공해주는 훅들을 써보자

이번에는 다수의 하윗값을 포함하는 복잡한 정적 로직을 구현하는 경우이 useReducer을 사용해야 한다고 하니 그렇게 작성하자

const [state, dispatch] = useReducer(reducer, {count: 0})

이 부분이 있을텐데 useReducer을 이용해서 reducer라는 함수와 두번째는 state의 초기값이 들어가게된다.

이때의 reducer에는 첫번째 인자로는 state(이전 상태), 두번째로는 state를 변경하려는 action(객체)가 들어오게 된다.

action 객체는 dispatch를 통해서 들어오게 된다.

App.js에 넣으면

잘 작동하는 것을 확인할 수 있다.

예를 들어서 위와같이 작성하면 persons의 계산 결과가 출력되지만 입력이 이루어질때마다 새로 render되어서 sum이 호출되는 것도 확인할 수 있다.

너무 비효율적이기 때문에 sum을 persons에 의존적으로 사용하겠다는 의미로 useMemo를 사용할 수 있다.

위와 같이 작성했는데 이러면 당연히 추가된다. 이럴경우 useEffect와 동일하게 Dependency list를 두번째 인자로 주면된다.

sum이 다시 계산되지 않는다.

이번에는 useCallback을 사용해보자. 이 안에 들어가 있는 함수를 언제 새로 셋팅해줄지를 dependency list에 의존적으로 결정해서 click안에 넣어준다. 이걸 사용하는 이유는 나중에 최적화에서 다시 배우도록 하자

useRef함수를 사용해보자.

Example7을 긁어와서 위와 같이 controlled component만 남기었다.

그리고 Ref 관련 함수들을 만들어보자.

Example8을 넣어주자

보면 input1은 null input2는 undefined

입력하면 input1Ref의 값은 render될때마다 계속 새로 ref를 만들어서 넣어준다. input2Ref는 render을 돌아도 계속 유지하는 것이다. 대신에 초기단계에서만 render된 적이 없기 대문에 undefined가 나온 것 뿐이다.

그러기 때문에 useRef는 계속해서 같은 ref를 가리킨다는 것을 확정할 수 있다.

createRef는 새로 ref를 만들어서 넣어주고 useRef는 새로운 것을 만들어서 넣어준다.

React Router Hooks

이번에는 withRouter가 아닌 react router hook을 사용하는 방법에 대해 알아보자.

useHistory, useParames

과거 React-Router-Example로 들어오자

useHistory는 props로 들어오는 history를 구해서 반환해준다.

LoginButton에서 사용했던 withRouter부분을 주석하고 그 내부 함수만 빼내자.

내용을 useHistory로 바꾸어보고 잘 실행되는 것을 확인하자

위와 같이 만드는 거이 보다 깔끔한 로그인을 만들 수 있을거 같다.

원래는 위와 같이 작성되어 있어서 페이지가 깊어지면 props를 전달하기 까다로웠다.

위와 같이 useParams를 사용하면 보다 깔끔하게 받을 수 있다.

위의 두개는 자주 사용하기 때문에 꼭 알아두자.

컴포넌트 간 통신

npx create-react-app component-communication

하위 컴포넌트 변경하기


import { useState } from "react"

export default function A() {
	const [value, setValue] = useState('아직 안바뀜')
	return (
		<div>
			<B value={value}/>
			<button onClick={click}>E의 값을 바꾸기</button>
		</div>
	)

	function click() {
		setValue('E의 값을 변경')
	}
}

function B({value}) {
	return (
		<div>
			<p>여긴 B</p>
			<C value={value}/>
		</div>
	)
}

function C({value}) {
	return (
		<div>
			<p>여긴 C</p>
			<D value={value}/>
		</div>
	)
}

function D({value}) {
	return (
		<div>
			<p>여긴 D</p>
			<E value={value}/>
		</div>
	)
}

function E({value}) {
	return (
		<div>
			<p>여긴 E</p>
			<h3>{value}</h3>
		</div>
	)
}

위와 같이 쭉 아래 함수로 값을 보내주어서 결국에 값이 바뀐다. 하지만 위의 일을 하는 과정에서 E를 바꾸어주기 위해 너무 많은 함수를 이동한다. 이러한 함수의 구조는 사고가 나기 쉽다.


상위 컴포넌트 바뀌기

export default function A() {
	const [value, setValue] = useState('아직 안바뀜')
	return (
		<div>
			<p>{value}</p>
			<B setValue={setValue}/>
		</div>
	)

	function click() {
		setValue('E의 값을 변경')
	}
}

function B({setValue}) {
	return (
		<div>
			<p>여긴 B</p>
			<C setValue={setValue}/>
		</div>
	)
}

function C({setValue}) {
	return (
		<div>
			<p>여긴 C</p>
			<D setValue={setValue}/>
		</div>
	)
}

function D({setValue}) {
	return (
		<div>
			<p>여긴 D</p>
			<E setValue={setValue}/>
		</div>
	)
}

function E({setValue}) {
	return (
		<div>
			<p>여긴 E</p>
			<button onClick={click}>click</button>
		</div>
	)

	function click() {
		setValue('값을 변경')
	}
}

버튼을 누르면 함수로 쭉 올라가서 바뀌게 된다.

아무튼 이렇게 하는 것은 상당히 비효율적이다.

Context API

npx create-react-app react-context-example

https://ko.reactjs.org/docs/context.html

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.

나중에는 또 이런 전역 속성을 관리하는 Redux가 나오기도 한다.

컨텐스트 생성 -> context API
.을 이용해서 대입

index.js의 가장 상위에 넣어주자

해서 위와 같이 App을 만든 Context로 감싸주고 Persons라는 객체 배열을 넘기어 주면 모든 컴포넌트가 사용 가능해진다.

첫번째 방식을 사용해보자

그래서 위와 같이 작성해서 App.js에서 띄어보자

잘 띄어지는 것을 확인할 수 있다. 이러한 방식으로 잘 사용된다.

위와 같이 뜬다. 하지만 이것의 문제점은 static이다 보니 여러개를 지정할 수 없다. 따른 context의 데이터를 가져다 쓸 수는 없고 다른 component로 받아서 props로 찔러주는데 그냥 이 방식은 쓰지 말자.

가장 많이 쓰는 것은 useContext Hook으로 데이터를 받아오는 것이다.

코드가 훨씬 깔끔하다

요즘에는 훅으로 쓰다보니 useContext 훅은 알아둘 필요가 있다.

React Testing

JavaScript Unit Test

가장 많이 쓰는 JEST를 사용해보자

mkdir jest-example
cd jest-example
npm init -y
npm i jest -D

1 + 2의 결과가 3이어야 한다는 테스트를 만들어보자

하면 위와 같이 test 된 것을 확인할 수 있다.

test.js
spec.js
인 파일들
__tests__/
폴더는 테스트 인것으로 인식된다.

실재 사용시에는 descibe 내부에 it(test 대신 작성)을 여러개 작성해서 테스트 해본다.

위와 같은 경우 객체의 비교는 틀린것을 확인할 수 있다. 아무래도 다른 메모리를 가리키다보니 틀렸다고 하나보다.

npx jest --watchAll 옵션을 주면 항시 테스트가 켜져있는 상태가 된다.

describe('expect test', () => {
	it('37 to equal 37', () => {
		expect(37).toBe(37)
	})
	it('{age: 39} to equal {age: 39}', () => {
		expect({age: 39}).toEqual({age: 39})
	})
	it('.toHaveLength', () => {
		expect('hello').toHaveLength(5)
	})
	it('.toHaveProperty', () => {
		expect({name: 'Mark'}).toHaveProperty('name')
		expect({name: 'Mark'}).toHaveProperty('name', 'Mark')
	})
	it('.toBeDefined', () => {
		expect({name: 'Mark'}.name).toBeDefined()
		//expect({name: 'Mark'}.age).toBeDefined()
	})
	it('.toBeFalsy', () => {
		expect(false).toBeFalsy()
		expect(0).toBeFalsy()
		expect(null).toBeFalsy()
		expect(NaN).toBeFalsy()
	})
	it('.toBeGreaterThan', () => {
		expect(10).toBeGreaterThan(9)
	})
	it('.toBeGreaterThanOrEqual', () => {
		expect(10).toBeGreaterThanOrEqual(10);
	});
	it('.toBeInstanceOf', () => {
		class Foo {}
		expect(new Foo()).toBeInstanceOf(Foo);
	});
	it('.toBeNull', () => {
		expect(null).toBeNull();
	});
	it('.toBeTruthy', () => {
		expect(true).toBeTruthy();
		expect(1).toBeTruthy();
		expect('hello').toBeTruthy();
		expect({}).toBeTruthy();
	});
	it('.toBeUndefined', () => {
		expect({ name: 'Mark' }.age).toBeUndefined();
	});
	it('.toBeNaN', () => {
		expect(NaN).toBeNaN();
	});
	it('.not.toBe', () => {
    expect(37).not.toBe(36);
  });

  it('.not.toBeFalsy', () => {
    expect(true).not.toBeFalsy();
    expect(1).not.toBeFalsy();
    expect('hello').not.toBeFalsy();
    expect({}).not.toBeFalsy();
  });

  it('.not.toBeGreaterThan', () => {
    expect(10).not.toBeGreaterThan(10);
  });
})

나머지는 알아서 jest 홈페이지에서 찾아보자


비동기 테스트 정보만 알아보자

// async test
describe('use async test', () => {
  it('setTimeout without done', () => {
    setTimeout(() => {
      expect(37).toBe(36);
    }, 1000);
  });

  it('setTimeout with done', done => {
    setTimeout(() => {
      expect(37).toBe(36);
      done();
    }, 1000);
  });
});

/// promise
describe('use async test', () => {
  it('promise then', () => {
    function p() {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(37);
        }, 1000);
      });
    }
    return p().then(data => expect(data).toBe(37));
  });

  it('promise catch', () => {
    function p() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error('error'));
        }, 1000);
      });
    }
    return p().catch(e => expect(e).toBeInstanceOf(Error));
  });
});

describe('use async test', () => {
  it('promise .resolves', () => {
    function p() {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(37);
        }, 1000);
      });
    }
    return expect(p()).resolves.toBe(37);
  });

  it('promise .rejects', () => {
    function p() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error('error'));
        }, 1000);
      });
    }
    return expect(p()).rejects.toBeInstanceOf(Error);
  });
});

// async 키워드 사용이 가장 좋다
describe('use async test', () => {
  it('async-await', async () => {
    function p() {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(37);
        }, 1000);
      });
    }

    const data = await p();
    return expect(data).toBe(37);
  });
});

describe('use async test', () => {
  it('async-await, catch', async () => {
    function p() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(new Error('error'));
        }, 1000);
      });
    }

    try {
      await p();
    } catch (error) {
      expect(error).toBeInstanceOf(Error);
    }
  });
});

React Component Test

npx create-react-app react-component-test

cd react-component-test

npm test

CRA에는 이미 jest가 설치되어 있어서 또다른 라이브러리 설치가 필요하지 않다.

보면 위와 같이 App.test.js 파일이 이미 제공되는 것을 확인할 수 있다.

App이라는 컴포넌트를 render함수로 render 해서 컴포넌트안의 내용을 확인하는 코드임을 알 수 있다.

package.json 파일을 확인해보면 위와 같이 테스트 라이브러리 파일들이 들어가 있는 것을 확인할 수 있다.

testing-library/react 활용하기

profile
신촌거지출신개발자(시리즈 부분에 목차가 나옵니다.)

0개의 댓글