...+131+139
=593,407

이 글은 수시로 업데이트하는 기술 공부 블로그 포스팅입니다. 이런거 공유하면 확실히 낚여줄 내주변 지인이 최소 1명 있긴 한데... 그에게는 이글이 대윾잼+대환장 띠용글로 보일것이지만 알고도 굳이 씁니다. 목표는 1년뒤 나역시 이걸 보며 대윾잼+대환장을 느끼는것


0. 서론

머 아무튼 2017년도 다 저물어가는 마당에 호주 워홀 마치고 돌아와서 취직은 해야겠고 마땅히 벌어놓은 것이라고는 각종 기획의 잔꾀와 워드프레스/코드이그나이터[각주:1]로 좀 굴러본 것뿐인지라… 최소한의 최신 기술과 개념을 익혀놓지 않으면 안 되겠다는 약간의 절박함이 생겨, 귀국 후 크리스마스 이브에 과감하게 세팅하고 시작한 것이 yuptogun.com 프로젝트.

지금까지 걍 구상만 했던 각종 웹앱들을 *.yuptogun.com 형태의 서브도메인 아래에서 각기 구현해서 돌리고, 개중 잘 팔리는 게 생기면 별도 도메인으로 독립시켜서 운영하고, 그러면서 호스팅업체 서버 스택 아래에서 할 수 있는 최대한의 신기술&신개념을 막 도입해 써보는 운동장으로 쓸 계획.[각주:2]


1. 스택: shared LAMP stack hosting + Codeigniter + uncompiled (in-browser) React

A. 서버 스펙. 일단 주어진 서버 환경을 서술하자면... 전형적인 한국 PHP 웹호스팅 스택. 리눅스에 아파치와 MySQL을 깔고 PHP를 상시 켜놓으며 가상 할당 공간에 실행&전송할 파일을 올리는 방식이다. 월 5천 원을 더 내면 SSH 접속을 시켜준다고 하지만 지금은 그마저도 하고 있지 않아서 그야말로 20세기 FTP 홈페이지 스타일. 쉘접속이고 서버설정이고 그딴거ㅇ벗다.

AWS도 있고 다른 옵션도 많은데 굳이 이걸 선택한 이유는 딱 두가지.

  • 비용: 닷홈의 혜자급 상품으로, 도메인값만 닷홈에 내면 디스크 1G와 트래픽 무제한 이용이 무료로 가능하다. 써보다가 트래픽이 증가하면 상위 상품으로 올릴수있음. (물론 하위 상품으로 다운그레이드는 안된다)
  • 시장 친숙성: 서비스 자체를 서비스하는 IT 기업을 제외하면 나의 '클라이언트'가 될 사람들은 보통 shared web hosting 환경에서 뭔가를 하려고 한다.[각주:3] 따라서 임대형 쉐어 웹호스팅 환경이라는 (프로그래머에게는) 꽤 열악한 스택을 선택하기로 함.

B. 프레임워크. PHP라는 서버 언어가 주어진 상황에서 제일 최신 프레임웍을 써볼라치면 아무래도 라라벨이나 심포니 같은 걸 해야겠지? 아니면 좀 힙하게 Slim? 사실 호주에 있을 때 이것저것 건드려 봤는데 결국은 그간 해왔던 코드이그나이터를 골랐다.

  • 가볍다. PSL-7을 준수하는 프레임워크들은 FTP로 배치하는 것이 사실상 불가능하다. 파일이 보통 많아야 말이지. 로컬에서 작업한 걸 통째로 zip으로 묶어 그걸 올리고 unzip을 (쉘로 하건 php코드로 하건) 실행하는 게 그나마 the best practice so far더라.[각주:4]
  • 버전 관리를 할 것이 없다. Ion Auth나 TCPDF 말고는 딱히 더 갖다 쓸 서드파티 라이브러리도 없고, 대부분의 요소들은 어차피 사용자단에서 돌아가야 하는 파일들인지라 그냥 CDN에서 퍼와도 되겠다고 판단했다.
  • Slim은 라우팅까지 성공했고 라라벨은 배치하는 것까지 성공했으나[각주:5] 지금은 백엔드 로직보다는 프론트엔드의 react 공부에 집중하고 싶어서 경험이 있는 Codeigniter를 채택. 보아하니 아직은 코드이그나이터가 php 실무 바닥에서는 현역인 듯도 하고.
  • 뭐 말이 Codeigniter지 가급적 API 생성기로만 사용하고, 각종 라이브러리나 유틸리티는 서버에서 돌아가는 게 효율적일 때(세션관리 등)만 쓸 예정. 애초에 서버란 JSON만 송수신해주면 되는거니까.

C. 뷰처리. 여기서부터 모험이 시작된다. views/template/react.php 파일을 만들고 여기에서 reactjs cdn이라고 검색해서 나오는 파일 두 개와 babel cdn이라고 검색해서 나오는 파일 하나를 불러오게 시켜놨다.

  • 사실 React 생태계가 추구하는 방식대로 하자면 절대 이렇게 해서는 안 된다. 특히 Babel은 내가 짠 text/babel 스크립트 파일을 서버에서 미리 컴파일하는 데 쓰여야 하지, 브라우저에서 낑낑거리면서 실행할 물건이 아니다.
  • 그치만 뭐 일단은 콘솔에 인포 메시지 하나 뜨는 거랑 깜박임 문제[각주:6] 말고는 기능상 별 문제를 모르겠어서 그냥 넘어가고 있는중.


2. 기본 흐름: CI는 json과 뷰파일 목록만 구성하고 나머지는 React가 한다

메인페이지를 만들면서 결정한 설계는 다음과 같음.

  1. 기본 라우팅에 따라 컨트롤러가 실행된다.
  2. 컨트롤러는 다음 세 가지를 주로 실행한다.
    • 로딩될 뷰파일을 결정한다.
    • 뷰파일에서 사용할 데이터를 만든다. SEO용 메타태그, 현재 로그인한 회원 정보 등
    • 뷰파일에서 json으로 출력할 배열을 만든다. 필요한 DB 테이블 쿼리 결과를 json_encode()로 뽑아 $this->data['table'] 형태로 넘긴다.
  3. 메인뷰는 다음 세 가지를 실행한다.
    • 컨트롤러가 넘긴 데이터를 가지고 html 마크업을 뿌린다. 메타태그, 로그인했을 경우와 아닐경우에 따른 html 마크업 등등
    • 컨트롤러에게서 물려받은 json을 스크립트 태그로 html 출력 맨끝에 뿌린다.
      <script>var theData = <?= $data; ?>;</script>
    • 그리고 assets들(React 라이브러리, 메인 js, 각종 CSS 등등)을 불러온다.
  4. 그러면 이제 뷰가 불러온 메인 js 파일은 브라우저 안에서 다음과 같이 작동한다.
    • 메인뷰가 서버 응답으로서 돌려주는 DB 데이터 json을 props/state로 참조한다.
    • 이 props/state와 자기 안에서 const로 규정돼 있는 정적 정보들(e.g. 쿼리결과 없을시 뿌릴 문자열들 등)을 이용해, 엽토군이 정의한 대로 열심히 컴포넌트 클래스들을 렌더링한다.
장황하게 적었지만 코드를 보면 아주 간단한 이야기다. 해당 페이지의 응답결과는 예컨대 위쪽에 <script src="app.js">가 있고, 중간에 <div id="wrapper"></div> 같은 텅 빈 요소가 하나 있으며 그 밑으로 조금 내려가서 보면 아주 장황하게 긴 json을 data 변수에 할당한 script가 한두 개 있는 식이다. 그리고 app.js는 data.table.forEach((record) => {어쩌고저쩌고}) 형태의 라인을 가진다.
어떤 assets을 불러올 것인지를 뷰에서 결정할 것인가 컨트롤러에서 결정할 것인가는 지금은 좀 가닥 안잡혀서 일단 죄다 뷰에서 결정중. 아마 컨트롤러에 다 때려넣는게 더 말은 맞겠지(극단적으로 말해서 뷰파일은 그냥 div#app 하나만 덜렁 갖고있어도 될지도 모른다).


3. 깨달은 것들: React

React를 막무가내로 공식문서 봐가면서 공부하고 적용해 현재까지 깨달은 것들은 다음과 같다.
  • 만약 당신이 MVC 구조에 익숙하고, 새로운 라이브러리의 문법을 배우는 데 큰 문제가 없으며, 지금 당장은 리액트를 가지고 화면에 DB자료를 뿌리는 정도로 충분하다면, 다른 것보다 Thinking in React를 따라가는 것이 가장 좋고 가장 빠르다. 당신이 알아야 할 것은 거의 대부분 나와 있으며, props와 state 개념을 실전으로 바로 알 수 있다. 장황하게 createClass가 어쩌고저쩌고 하는 설명도 생략돼 있어, 필요한 기초에 집중할 수 있다.
  • state는 최상위 컴포넌트에서 통제되어야 한다. 글로 읽을 땐 영 뭔말인지 몰랐는데 실전을 해 보니까 간신히 이해가 되더라.
    예컨대 이런 상황이 있다고 생각해 보자.
#wrapper가 #sidebar와 #content의 2개 요소를 가지고 있고, #sidebar 밑의 a[data-tab=x]를 누르면 #content 밑의 #x가 보여야 함

jQuery라면 구현 자체는 아주 간단하다. .data('tab')으로 x값 얻어와서 $('#content')를 통제할 때 그 값을 넘기면 된다. 하지만 이 구현이 일으키는 변화는 필연적인 것이 전혀 아니며 오히려 아주 우연하고 작위적이다.
이 상황을 React는 이렇게 해결한다.

#wrapper는 #sidebar 밑의 a에 대해서도 책임이 있고, #content 밑의 div들에 대해서도 책임이 있다. 따라서 #wrapper의 this.state.tab을 정의해 주고, #content는 항상 이 state에 대응하는 div를 보여주도록 한 다음, #sidebar 밑의 a가 눌릴 때는 #wrapper의 this.state.tab이 적절히 업데이트되도록 한다. 그러면, #content 밑의 div는 즉시 자동으로 그 state에 반응("react")한다!

이렇게 써놓고 보니 왜 다들 그렇게 열광하는지 알것도 같음. 데이터가 어디로 흐르는지 볼 수 있고 훨씬 더 논리적이다. (단 그만큼 프론트엔드가 초반에 머리 싸매고 시작을 해야 하는 것. 어느 객체가 최상위인가? 정말 모두 한 개의 파일/스크립트 안에 넣어야 할까?)

  • 문자열을 직접 인젝션하는 코딩은 잘 안 된다. 예컨대 html 마크업을 구성하는 문자열은 이스케이프되어 문자그대로 브라우저에 뿌려진다. 정 그걸 띄우고 싶다면 악명높은 dangerouslysetinnerhtml을 쓰든지 차라리 리액트 html 오브젝트로 넣어버리라는 것이 공식 조언이다.
    또 남들은 다 된다고 하는데 나만 안 되는 것이 뭐나면… 문자열을 전달받아 그 문자열과 일치하는 이름의 컴포넌트 렌더링하기. 흔히 알려진 React.createElement(ChildName, null) 용법이 작동을 안 한다.[각주:7]
  • render () 메소드 내에서 const로 만드는 빈 배열은 길이가 0이 아니라 1이다. 이유는 모르겠다. 그래서 forEach push로 필터링을 할 때 결론적으로 이런 식의 이상한 판별식을 쓰고 있다. 이게 아닌데... 분명 더 옳은 방법이 있는데...
if (filteredArray[0].props.name != '') { /* do the stuff */ }
  • PHP가 세션/쿠키를 만드는 과정/방법론 때문에 사용자 인증이 생각보다 번거롭다. Ion Auth를 쓰고 있는데, 최종적으로는 기껏 깔아놓은 react router를 못쓰고 결국 자기 자신에게 로그인폼을 POST 제출해서 로그인 처리하고 리디렉션 시키는 몹시 전통적인 짓을 하고 있다.
    • 원래 계획은 ajax 폼으로 그 자리에서 즉각 로그인을 시키는 것이었다. 로그인 자체는 성공했는데, 문제는 자동로그인("keep logged in") 기능. 자동로그인이 도무지 먹지를 않는다. 왜 그런가 하고 모든 소스를 다 뜯어본 결과… "자동로그인"은 일반적으로 이런 로직으로 동작한다는 것을 알았다.
    1. 사용자가 POST 요청으로 로그인을 시도한다.
    2. PHP는 ID, 해싱된 패스워드, 자동로그인 여부를 받아 로그인(유저정보 매칭)을 시도한다.
    3. 로그인이 성공하면 PHP는 지금 로그인한 세션 고유값을 DB에 저장한다.
    4. 그리고 즉시 HTTP 응답 헤더에 이 세션 고유값이 포함된 쿠키를 쓰라는 요청을 보내는 리디렉션을 실행한다.
    5. 이때 PHP가 별다른 출력을 내지 않아야 비로소 이 쿠키가 써진다.[각주:8]
    6. 리디렉션된 HTTP 응답에서부터 쿠키와 세션은 유효해진다.
    7. 그래서 그 다음부터 (리액트 라우터가 아닌) PHP가 HTTP 요청을 받으면 우선 서버단에서 쿠키와 세션을 확인해 로그인 여부를 체크하고 그제서야 그 결과를 반환해 이용할 수 있게 한다. 여기서 자동로그인 구현 완료.
    • 여기서 5번 스텝이 문제되는 것이었다. ajax 요청은 필연적으로 출력을 낳고, 따라서 자동로그인 로직의 5번에서 멈춘다.[각주:9]
    • 그러면 ajax 요청 콜백에서 적당히 쿠키를 만들면 되는것 아니냐? 싶을 텐데 그러면 7번 스텝이 걸림. 요컨대 자동로그인을 체크하고 로그인해서[각주:10] → 사용자 정보를 가져온 뷰를 → F5로 새로고침하면[각주:11] → 거짓말처럼 로그아웃당함.[각주:12]
    • 서버단이 JS로만 구현되었거나 자동로그인 로직이 좀 달랐다면[각주:13] ajax 콜백의 쿠키 작성만으로도 충분히 자동로그인을 할 수 있었을까 하는 미련이 남지만... 그건 먼 훗날 많은 이치를 깨닫고 나서 다시 보기로. 지금은 우선 로그인을 시켰다는 데 의의를 두면서, u/(.*) 라우팅과 컨트롤러의 u() 통제와 users.js 최적화로 타협한다.[각주:14]

99. 해야될것들

  1. Redux 배워서 써먹기
    • "최상위 컴포넌트에 스테이트 때려박는게 너무 귀찮아서 온갖 방법으로 그걸 회피하는데요 그래서 state management 라이브러리를 씁니다...!"
    • 아직은 귀찮거나 헤비하지 않지만 넘나 당연하게 react의 짝패로 쓰이는 모양이니 싫어도 적용해야할듯.
  2. webpack 배워서 써먹(으면서 필요한 의존성 버전관리하)기
    • "근데 리액트 사용의 정석은 php 파일에서 로드하는거는 아니에요 webpack으로 빌드해서 index.html 및 js / assets 뭉치를 만들고 걔를 static hosting"[각주:15]
    • 지금 당장은 이런 스텝들이 필요할듯
      • React, Babel을 npm으로 받아서 로컬 트리에 넣어놓기
      • 지금 있는 메인 js를 번들 가능하게 조정하기
      • 컨트롤러 $dev 변수에 따라서 지금식으로 다 로드하느냐 bundle-*.js만 불러오느냐를 스위칭할 수 있게. (이건 순 내 편의를 위한 편법. 충격적이게도 지금은 메인 js 고칠때마다 서버에 업로드하고 새로고침을 해서 테스트를 본다고 한다...)
    • 뭐 일단 해놓으면 앞으로는 npm update 한번씩만 돌리면 되겠지
      • 잘은 몰라도 나중되면 지금 뷰파일이 로딩중인 fontAwesome, pureCSS 등등도 죄다 이걸로 관리하게될듯


  1. 그리고 대략 다섯 달 동안이지만 충격과 공포의 해피CGI 솔루션(...)을 경험해봤다. PHP 5.2 이하에서 실행해 달라고 요구하는 솔루션을 들어보신 적이 있는지? 한국에서는 무려 이새끼가 아직도 현역이다. 팩트TV가 이회사 물건으로 돌아가고 있다. [본문으로]
  2. 어차피 만들려는 웹앱들의 본질이 TODO를 크게 벗어나지 않기 때문에 가능한 짓이다. [본문으로]
  3. 보통은 애초에 "서버언어" 개념 자체를 안 갖고 계시니. [본문으로]
  4. 예전의 다른 웹호스팅에 라라벨 올리면서 찾은 방법. 다시 하기는 싫다. [본문으로]
  5. 일단 로컬 구조 그대로 올린다 → 루트의 index.php가 public/index.php 를 require하도록 고친다 → public/index.php 가 ../bootstrap/autoload.php 와 ../bootstrap/app.php 를 require하도록 한다. 이렇게 하면 FTP만 되는 호스팅 환경에서도 일단 라라벨을 굴릴 수는 있음. [본문으로]
  6. 인브라우저 컴파일 중일 때는 아무것도 안 뜨다가 잠시 후에야 가상DOM이 그려지는 문제. 리액트 사용상의 대표적인 대환장 포인트 중 하나라고. [본문으로]
  7. 선배왈 스위치문 써서 컴포넌트 리턴하는 거 말고는 답이 없다고. 안그래도 나도지금 그렇게 하는중이다. 글쎄 이게 아닌데... [본문으로]
  8. 내가 이해한 바로는, 쿠키 생성 코드가 포함된 코드 라인들의 최종 목적이 '뭔가를 출력하는 것'이어서는 안 된다는 느낌이다. 예컨대 redirect()는 괜찮지만 return $resultjson은 안 된다. [본문으로]
  9. 별짓을 다해 확인해 본 결과, 세션도 만들어지고 쿠키 요청도 들어가지만 실제 쿠키 생성부터가 안된다는 것으로 판명. CORS도 확인하고 별짓을 다해봤는데 그게 문제가 아니라 결국 setcookie() 함수의 결함이랄까 설계 때문에 안된다. [본문으로]
  10. 여기선 클라이언트(브라우저)가 단지 쿠키만을 작성한다. [본문으로]
  11. 여기선 서버가 존재하지 않는 로그인정보 세션을 체크하려고 시도한다. [본문으로]
  12. 매치해 볼 세션이 없으므로 자명한 결과. [본문으로]
  13. 대충 찾아보니 JWT(JSON 웹토큰)과 로컬스토리지 사용이 JS 환경에서는 가장 통념으로 사용되는 듯하다. [본문으로]
  14. 그리고 (서버와 리액트가 라우팅을 놓고 싸우는 탓에) 꽤 속도가 느려졌으며 여전히 두세 시간 뒤면 세션이 풀려버린다. 왜죠... [본문으로]
  15. 느닷없이 그런 말을 들어도 지금은 잘모르겠는 부분이지만 여튼 이바닥 선배가 그렇다고 하니 그런줄 알자. [본문으로]
Posted by 엽토군

댓글을 달아주세요:: 네티켓은 기본, 스팸은 사절


카테고리

분류 전체보기 (782)
0 현재 호주워홀 (6)
1 내 (325)
2 다른 이들의 (251)
3 늘어놓은 (34)
4 생각을 놓은 (69)
5 외치는 (69)
9 도저히 분류못함 (27)

달력

«   2018/11   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  
Statistics Graph