모임어플 Moyeo

 부트캠프 동기들 3명과 함께 외로운 현대인들을 위한 모임어플을 하나 만들어 보았습니다.

그 이름은 Moyeo입니다. 저는 Front-end를 맡았습니다.

어플의 대략적인 UI View를 캡쳐해보았습니다.

 

 

 Moyeo 어플은 실시간 다중 채팅과 모임 결성을 위한 투표기능을 갖는 모임 어플입니다.

카카오맵 API사용 은 시간상 구현하지 못했지만, 실시간 다중채팅과 투표기능을 처음으로 React-native만으로 구현한 것에 큰 의미가 있다고 생각합니다. 그리고 스크린에 해당하는 컴포넌트의 수를 줄였으면 좀더 UI를 이쁘게 만들수 있었을텐데 너무 아쉽네요.

 

 

Skills 및 개발환경

React-native, Expo, Github, Github-flow

Expo를 사용하면 react-native cli가 아니라 expo cli를 통해서 앱을 실행하게 되는데, expo라는 런타임 위에서 앱이 작동되어서 그런지 react-native cli를 통해서는 그냥 사용 가능한 모듈이 expo를 통해서는 사용할 수 없는 경우가 많았습니다. expo에서 제공하는 모듈이 많이 있기는 하지만, 다른 모듈이 필요한 경우 난감했습니다. 그리고 라이브러리를 사용할 때도 expo 위에서도 문제없이 이 라이브러리가 작동 할 것인지가 항상 걱정이었습니다. 

 

 

 

프로젝트 진행

Agile scrum방식에 따라 프로젝트를 진행하였습니다.  http://bitly.kr/mJcJOL

 React를 배우고나면 React-native는 쉽게 할 수 있다라는 말을 많이 들었습니다. 그런데 React-native를 처음 사용하면서 느낀점은, 아무리 쉽다고 해도 처음 해보는건 다 어렵다는 것이었습니다. React-native와 익숙해지는 과정 자체가 시간이 걸렸습니다. 짧은시간에 프로젝트를 완성해야 했기 때문에 Android와 IOS를 동시에 개발하지는 못했고, 일단 시뮬레이터를 통해 Andoird의 레이아웃만 확인하면서 프로젝트를 진행하였습니다. 

 

 

 

 

React-Native 와 Expo

 이번 프로젝트에서 중요하게 생각해 보고, 또 공부해 보아야 할 부분은 RN-cli와 expo-cli의 차이점 이라고 생각합니다.

다른 기능적인 부분이야 다른 프로젝트를 통해서도 시행착오를 겪으며 공부해 나가면 되겠지만, 아마 앞으로 이 expo를 이용해서 RN 프로젝트를 할 일은 없을 것 같기에...(expo 환경 위에서 작동이 잘 안되는 부분들이 너무 많았습니다ㅜㅜ..)이번에 잘 공부해서 이곳에 정리해 두면 좋을 것 같네요.

 

- Expo

 

 

- React-native cli

 

 

 

 

 

 

 

 

 

참고자료

http://bitly.kr/RlMfZ1T

Humanscape와 협업

이번에는 Humanscape 회사와 협업으로 프로젝트를 하나 진행했습니다.

PGHD 어플을 만드는 것이었는데요. PGHD란, Patients Generated Health Data 의 약자입니다. 의사가 진료를 통해 기록한 환자의 데이터가 아니라, 환자 자신이 기록한 데이터를 다루기 위한 App입니다. 

 

출처: https://humanscape.io/kr/

 위 사진 자료는 휴먼스케이프 홈페이지에서 찾아볼 수 있는 자료입니다. 위 스크린샷을 통해 이 프로젝트가 어떤 프로젝트인지 그 내용은 짐작이 가시리라 생각합니다.  병원 좀 다녀보신 분들이나, 몇번 크게 아파보신 분들은 대부분 공감하실겁니다. 의사가 알고 있는 임상학적 통계자료에 근거한 환자의 몸 상태와, 환자가 실제로 느끼는 자신의 몸 상태에는 조금 괴리가 있습니다. 아마도 이러한 괴리감은 단순히 환자의 과민반응이라기 보다, 질병의 진화에 발 맞추어 업데이트가 일어났어야 할 임상학적 의료 데이터가 뒤처진 결과일지도 모릅니다. 의학계가 다루는 임상학적 질병 데이터와, 실제 환자들이 경험하는 질병 데이터간의 괴리감을 줄이는 것이 휴먼스케이프가 진행하는 프로젝트의 목적이 아닌가 하는 생각이 들었습니다. 개인적으로 멋진 프로젝트라는 생각이 들었습니다.

 

 

 

 

Skills 및 개발환경

React-native, React-native cli, Redux, Github, Git-flow, SourceTree를 사용하여 개발하였습니다.

 

 

 

 

프로젝트 진행

 Agile scrum방식에 따라 프로젝트를 진행하였습니다.    http://bitly.kr/LHD1jI      

 저는 이번 기업협업 4주 프로젝트에서 Front-end 담당으로, React-native와 React-navigation으로 app의 회원가입 기능을 구현했습니다.

제가 맡은 스크린 중에 하나인 회원가입 스크린은 완료버튼이 위치하는 최상위 스크린의 역할을 하는 컴포넌트와, 이 최상위 스크린을 구성하는 컴포넌트들로 구성되어있습니다. mapping되는 컴포넌트들을 제외하면, 총 7개의 컴포넌트로 구성되어있구요.

아래 사진의 빨간색 박스들은 모두 하나의 컴포넌트입니다.

 

 

 간단하게 구현할 수 있을 것 같았지만, 실제 기업이 요구하는 회원가입의 구현은 복잡했습니다.  input태그의 입력에 있어서는, 유저가 어떤 문자를 입력하고 있는지를 파악해서 적절한 입력을 요구하는 경고문을 그 아래에 띄워야 했습니다. fetch 요청시에  email이 중복된다던가 하는 것도 구분해서 다른 경고문을 띄워야 했고, 이러한 조건들이 password, nickname에도 적용되어있어야 했습니다. email, password, nickname 모두 조금씩 다른 로직이 들어갑니다. 약관동의 또한 선택적인 동의사항 부분을 제외하고는 필수 동의사항은 유저가 동의해야만 회원가입이 가능하도록 코딩을 해야했습니다. 각각의 하위 컴포넌트들에서 이러한 조건들의 만족 여부를 최상위 컴포넌트에 항상 알려주도록 해야했고, 유저가 마지막으로 어떤 컴포넌트에 대한 입력을 완성하던 이 모든 조건들의 만족여부를 따져서 완료버튼이 활성화 되게 해야 했습니다. 활성화 된 이후에 갑자기 유저가 password에 허용되지 않는 특수문자를 하나 추가한다면, 그 즉시 완료버튼은 회색으로 변하면서 비활성화 되어야합니다. 버그가 없도록 하기 위해 많은 경우의 수를 생각하거나 테스트 해봐야 했고, 유저가 어떤 순서로 회원가입 입력을 수행하던, 유저의 행동에 대응해서 적절한 경고문을 띄우는 로직이나 완료버튼의 활성/비활성 로직은 문제없이 작동하도록 만들어야 했습니다.

 

 회원가입 입력 양식을 지키지 않은 경우, "완료" 버튼은 비활성화 상태입니다.

email, password, nickname, 그리고 필수적으로 동의해야 하는 약관에 동의를 완료했을때, 회원가입 스크린은 "완료"버튼이 활성화 된 채로 유저가 버튼을 클릭하기를 기다릴 것입니다. 

 

 

 

 

 하지만 최초로 이 모든 로직을 완성하고, 테스트를 하면서 모든 회원가입 유효성 조건을 만족시켰을때 시뮬레이터의 화면은 붉게 물들었습니다.

 

 유저가 완료버튼을 활성화 시키기 위한, 유효성 조건을 만족시키는 마지막 액션을 취했을때 최상위 컴포넌트는 render() 를 실행했을 것이고, 최상위 컴포넌트가 재 렌더링됨과 동시에 그 스크린을 구성하는 자식 컴포넌트들도 모두 재렌더링됩니다. 현재 완료버튼을 활성화 시키는 조건이 모두 만족된 상태이기 때문에, 자식컴포넌트의 어디엔가 위치하는 최상위 컴포넌트의 render()를 실행하도록 하는 로직이 다시 읽혀버렸고... 이렇게 무한 렌더가 일어나면서 콜스택이 터졌습니다. 회원가입의 유효성을 만족하기 위한 마지막 액션을 유저가 취한 이후로 최상위 컴포넌트의 state는 아무 변화가 없었지만, setState가 계속해서 실행되면서 render()가 무한히 실행되어버렸습니다. 이리저리 다른 조건들을 자식컴포넌트들의 로직에 추가하고, 코드를 좀 더 복잡하게 만든 결과, 오류없이 제대로 작동하게 되었습니다. 기능상 문제가 없도록 코드를 짠 다음 내 코드를 보니까, 최상위 컴포넌트의 state가 너무 많아졌고 내부 코드의 로직이 복잡해져서 좀 더 알아보기 쉬운 코드로 만들고 싶어졌습니다. state 로직들을 좀더 깔끔하게 만들기 위해  Redux 적용해보기로 했습니다.

 

 

 

Rendering 최적화

 한번 무한 렌더링을 경험하고 나니까, 유저가 어떤 액션을 취할 때 마다 어떤 컴포넌트들이 렌더되고 있는지 확인하고 싶어졌습다. 모든 컴포넌트의 render( ) 아래에 console.log("<컴포넌트이름>.tsx 렌더") 코드를 집어넣었고, 유저의 액션에 대해 어떤 컴포넌트가 렌더링 되는지 렌더링을 테스트하면서 코딩을 진행했습니다.

 redux는 state를 변화시켜야 할 때 reducer를 통해 항상 새로운(참조가 다른) state를 만들어서 기존의 state를 대체하도록 하고 , state가 이전과 다르다면 컴포넌트의 render() 함수가 재실행되도록 합니다. 이때 reducer는 shallow compare를 수행해서 새로운 state가 이전의 state와 value 수준에서 같은 형태라면 상태가 변하지 않았다고 판단하고 render()를 재실행하지 않습니다. 덕분에 무한 렌더링을 방지하기 위한 로직은 필요가 없어졌고, 코드가 보다 간결해졌습니다. 그런데 여기서 한가지, '이래도 되나?' 싶은 부분이 눈에 보였습니다.

 

 

 

 영단어 3개를 입력했는데 렌더링이 24번 일어났습니다. 회원가입 최상위 컴포넌트인 스크린과 그 스크린을 구성하는 컴포넌트들은 모두 Redux의 store와 이어져 있습니다. store의 state가 변한다는것은 이 store와 이어져 있는 모든 컴포넌트의 상태가 변한 것이 됩니다. 따라서 유저가 email input 엘리먼트에 한 단어를 타이핑 할 때마다 회원가입 스크린을 구성하는 모든 컴포넌트들이 다시 렌더링 됩니다. 저는 이것이 불필요한 렌더링이라고 생각했습니다.

 

 물론 console.log로 크롬 개발자도구에서 확인할 수 있는 렌더 횟수는 React의 가상 Dom(Virtual DOM)에 반영되는 부분이고, 실제 Dom에 반영 될 때는 react의 diffing 알고리즘(Reconciliation)에 의해 변화된 부분만 실제 Dom에 렌더링됩니다. Reconciliation은 React의 Virtual DOM과 실제 DOM을 비교해 DOM을 갱신하는 작업입니다. 즉 React는 내부적으로 변경된 부분만 감지하여 실제 Dom의 렌더가 일어나도록 구현되었기 때문에, 사실상 불필요한 Dom의 렌더는 최소화 하도록 되어있습니다. 하지만 실제 Dom에 반영 할지 말지를 결정하는 이러한 과정, 즉 가상Dom에서 각 컴포넌트에 대해 recursive하게 일어나는 Reconciliation과정은 복잡한 연산이 될 수 있고 이 과정의 규모가 커진다면 App의 성능에 영향을 줄 것입니다. 내가 만든 App의 규모가 더 커질수록, App의 성능에 문제가 될 수 있는 부분입니다. ( React의 Virtual Dom을 통한 diffing algorithm 적용은 React-Native에서도 같은 방식으로 작동합니다 )

 

 그러면 이건... 로직의 복잡성을 감안하고 react만 사용해서 render횟수를 줄임으로써 cpu에 가는 부담을 줄이는 것과, 로직을 심플하게 가져가고 코딩을 편하게 하기위해 redux를 사용하지만 render 횟수의 증가로 cpu에 부담을 주는것... 이 두가지 사이에서 잘 선택해야 하는 문제인가? 라고 처음엔 생각했습니다. 이 문제에 대해서 이런저런 자료들을 찾아보니, 이건 내가 처음 생각한 양자 택일의 문제가 아니라 최적화에 대한 문제였습니다. 내가 React를 잘 사용할 줄 안다면, redux를 사용하면서도 불필요한 렌더를 없애고 훨씬 좋은 성능을 내도록 할 수가 있습니다.

 

 render라는 것은 화면을 통해 무언가를 유저에게 보여준다는 것입니다. 일반적인 render라는 개념에 있어서, 최적화에 대해 깊게 들어가면시스템 수준에서의 Process와 thread부터 공부해야할 것 같습니다. 지금은 우선 React라는 프레임워크 내부의 컴포넌트가 적절하게 렌더링 되도록 하는 기술의 활용 수준에서 알아보았습니다. 

 

 React가 불필요한 렌더링을 수행하지 않도록 하기 위한 두가지 대표적인 방안이 있습니다.

 

1. shouldComponentUpdate 라이프사이클 메소드 사용 

 React의 shouldComponentUpdate 메소드의 기본 리턴값은 true 이기때문에 이 메소드를 아예 사용하지 않는다면, 부모 컴포넌트의 render()가 실행 되었을때 그 자식 컴포넌트들은 자신의 상태가 이전과  동일하더라도 실제 Dom에 렌더링만 일어나지 않을 뿐이지 항상 render()가 호출되게 됩니다. React가 제공하는 shouldComponentUpdate 메소드를 통해 재 렌더링 여부를 결정하도록 할 수 있습니다. 하나의 컴포넌트 내부에 속해있는 엘리먼트의 레벨이 깊어지면 깊어질 수록,  shouldComponentUpdate를 잘 사용하는 것이 성능에 큰 영향을 주게됩니다.

 

2. React.PureComponent 활용 

 PureComponent란 shouldComponentUpdate 메소드의 기능이 이미 내장된 컴포넌트입니다. 따라서 PureComponent는 그 내부에 shouldComponentUpdate라는 메소드를 정의하고 있지 않습니다. PureComponent가 호출되면 그 자체적으로 이전의 state와 현재의 새로운 state를 비교해서 render()의 호출 여부를 결정합니다.

 아니 그러면 그냥 무조건 React.PureComponent를 사용하면 되지, 왜 일반적인 Component에서 shouldComponentUpdate 함수를 사용하느냐? 그건 PureComponent가 이전 state와 현재의 새로운 state를 비교할때 shallow compare 방식을 사용하기 때문입니다. shallow compare는, shallow copy와 그 의미는 동일하지만 작동 방식과는  조금 다릅니다. shallow compare는 reference가 아니라 value를 비교합니다. 말 그대로 얕은 수준에서 state라는 객체 내부의 값(value)들만 비교하여 이 state가 이전과 동일한지 아닌지를 판단합니다. 따라서 만약 state가 nested된 구조로 depth를 가진다면, 즉 state의 내부에 그 값으로써 object가 있다면  shallow compare로는 상태를 제대로 비교 할 수가 없습니다.  nested된 자료구조 내부의 값을 하나 변경했다고 해도, 그 object가 메모리상에서 여전히 같은 주소값을 가지고 있다면 React는 상태가 변하지 않았다고 판단 할 것이고, render()는 재실행 되지 않습니다. state가 이렇게 복잡한 구조를 갖는 경우에는 일반적인 Component를 사용하고 직접 shouldComponentUpdate 내부에 deep compare를 수행하는 상태 비교 로직을 구현해야 합니다. 그런데 React rendeing의 최적화를 위해 shouldComponentUpdate를 사용하는데, 그 내부에서 deep compare를 수행한다면 오히려 성능이 더 안좋아 질수도 있습니다. state가 아주 복잡한 depth를 지닌다면, deep compare를 수행하는데 또 오랜 시간이 걸릴지도 모릅니다. React가 자체적으로 제공하는  PureComponent가 shallow compare를 기본 작동 방식으로 하는 것은 React의 state를 최대한 단순하게 사용하라는 권고사항일지도 모르겠습니다. 

 

 

 

 

 

React는 잘써야 빠르다

 React는 시작하기 쉽고, 사용하기에 간단합니다. 하지만 React가 항상 더 빠르다는건 잘못된 생각입니다. 아래는 Redux 개발자인 Dan Abramov의 트위터 글입니다. 구글에서 "dan abramov react faster"라고 검색해서 맨 위에 나오는 링크로 들어가면 됩니다. 쭉 읽어보면 React 빠르다는 것이 어떤 의미인지 짧고 명확하게 설명하고있습니다.

 

 

 

Federico Zivolo라는 사람과 조금 길게 React의 속도에 대해 대화를 하고 있는데, 쭉 읽어보면 이런 내용이 나옵니다.

 

 

 

  브라우저가 유저에게 화면을 보여주기 위해서는 Dom 노드로 이루어진 트리를 만들고 layout 배치를 계산하여 적절하게 각 노드들을 위치시키고 Painting을 입히는 작업이 필요합니다. React가 빠르다는 표현은, 이 과정이 매번 처음부터 새롭게 이루어 지는 것이 아니라 변화된 부분에 대해서 한번만 일어나기 때문에 나온 말입니다. 브라우저 내에서 일어나는 연산의 양을 줄인다는 측면에서 React는 빠릅니다. 하지만 Dan Abramov가 자신의 트위터에서 말했듯이, React가 각각의 Component에 대해 recursive하게 diffing 알고리즘을 적용하는 과정은 expensive한 과정입니다. 이 과정은 충분히 느릴 수 있다는 것을 알아야 합니다. rendering부분에만 국한된 것이 아니라, 다른 많은 부분에서도 최적화를 위해 고민해야  성능좋은 React 앱을 만들 있습니다. 

 

 프로젝트의 규모가 커지고 내 앱이 많은 기능을 내포하게 될수록 성능에 대해 반드시 고민하게 될 것입니다. 사람들은 느린 어플은 사용하지 않습니다. 최적화에 대해 많이 고민한 만큼 내 어플은 빨라집니다. React의 동작 원리를 이해하기위해 더 공부해야만 한다는 것을 느꼈습니다. 버그없이 작동하는 어플을 만드는 것 뿐만 아니라, 어플이 느려질 수 있는 상황들을 찾아서 해결하는 것 또한 중요하다는 것을 알게 되었습니다. 

  

 

 

 

 

References

http://bitly.kr/6QXL1U

http://bitly.kr/o6iXkf
http://bitly.kr/cogUPdJ

http://bitly.kr/IBtF5zu

http://bitly.kr/dJfQCX
http://bitly.kr/i7pB8DH
http://bitly.kr/9FbBWh
http://bitly.kr/IvsVOa
http://bitly.kr/6QzLJdY
http://bitly.kr/zR8uIij
http://bitly.kr/26c2Pv
http://bitly.kr/1SfymG

http://bitly.kr/63oJOt

http://bitly.kr/TkERAD
http://bitly.kr/kMOKjJ
http://bitly.kr/IW8DjT

'프로젝트' 카테고리의 다른 글

Moyeo 프로젝트  (0) 2019.08.20

+ Recent posts