리액트애서 애플리케이션을 만들 때, 기본적으로는 보통 하나의 루트 컴포넌트 (App.js) 에서 상태를 관리합니다. 예를들어서,투두리스트프로젝트에서는, 다음과 같은 구조로 상태가 관리되고 있죠.
리액트 프로젝트에서는 대부분의 작업을 할 때 부모 컴포넌트가 중간자 역할을 합니다.
컴포넌트 끼리 직접 소통 하는 방법은 있긴 하지만, 그렇게 하면 코드가 굉장히 많이 꼬여버리기 때문에 절대 권장되지 않는 방식입니다. (ref 를 사용하서 이러한 작업을 할 수 있긴 하죠.)
App 에서는 인풋의 값인input값과, 이를 변경하는onChange함수와, 새 아이템을 생성하는onCreate함수를 props 로 Form 에게 전달해줍니다. Form 은 해당 함수와 값을 받아서 화면에 보여주고, 변경 이벤트가 일어나면 부모에게서 받은onChange를 호출하여 App 이 지닌input값을 업데이트 하죠.
그렇게 인풋 값을 수정하여 추가 버튼을 누르면, onCreate 를 호출하여 todos 배열을 업데이트 합니다.
todos 배열이 업데이트 되면, 해당 배열이 TodoItemList 컴포넌트한테 전달이 되어 화면에 렌더링 되죠.
이런식으로, App 컴포넌트를 거쳐서 건너건너 필요한 값을 업데이트 하고, 리렌더링 하는 방식으로 프로젝트가 개발됩니다.
이러한 구조는, 부모 컴포넌트에서 모든걸 관리하고 아래로 내려주는 것익 때문에, 매우 직관적이기도 하고, 관리하는 것도 꽤 편합니다. 그런데 문제는 앱의 규모가 커졌을 때 입니다.
보여지는 컴포넌트의 개수가 늘어나고, 다루는 데이터도 늘어나고, 그 데이터를 업데이트 하는 함수들도 늘어나겠죠. 그렇게 가다간 App 의 코드가 엄~~~~~ 청 나게 길어지고 이에 따라 유지보수 하는 것도 힘들 것입니다.
예를 들어 다음과 같은 구조의 프로젝트가 있다고 생각해봅시다.
Root 컴포넌트에서 G 컴포넌트에게 어떠한 값을 전달해 줘야 하는 상황에는 어떻게 해야 할까요?
A 를 거치고 E 를 거치고 G 를 거쳐야 합니다. 아! 근데 이걸 또 코드로 작성해가면서 해야하죠.
// App.js 에서 A 렌더링<A value={5}>// A.js 에서 E 렌더링<E value={this.props.value}/>// B.js 에서 G 렌더링<G value={this.props.value}/>
그러다가 value 라는 이름을 anotherValue 라는 이름으로 바꾸는 일이 발생한다면요? 파일 3개를 열어서 다 수정해줘야하죠.
리덕스를 쓰면, 상태 관리를 컴포넌트 바깥에서 한다!
리덕스에 대한 설명은 여러가지 방식으로 할 수 있겠지만 저는 주로 이런 표현을 합니다. 리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 됩니다.
그림으로 설명하자면 다음과 같은 구조죠.
예를 들어서 B 에서 일어나는 변화가 G 에 반영된다고 가정을 해봅시다.
스토어 설정
리덕스를 프로젝트에 적용하게 되면 이렇게 스토어 라는 녀석이 생깁니다. 스토어 안에는 프로젝트의 상태에 관한 데이터들이 담겨있죠.
컴포넌트의 스토어 구독
G 컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.
스토어에 상태 변경하라고 알려주기
이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.
예를들어{ type: 'INCREMENT' }이런 객체를 전달 받게 된다면, 리덕스 스토어는 아~ 상태에 값을 더해야 하는구나~ 하고 액션을 참조하게 됩니다.
추가적으로, 상태값에 2를 더해야 한다면, 이러한 액션 객체를 만들게 됩니다:{ type: 'INCREMENT', diff: 2 }
그러면, 나중에 이 diff 값을 참고해서 기존 값에 2를 더하게되겠죠. type 를 제외한 값은 선택적(optional) 인 값입니다. 액션에 대해선 나중에 더 자세히 알아볼게요.
리듀서를 통하여 상태를 변화시키기
액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야겠죠? 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부릅니다. 이 함수는 나중에 우리가 직접 구현하게 됩니다. 예를들어 type 이 INCREMENT 라는 액션이 들어오면 숫자를 더해주고, DECREMENT 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 되죠.
리듀서 함수는 두가지의 파라미터를 받습니다.
state: 현재 상태
action: 액션 객체
그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.
상태가 변화가 생기면, 구독하고 있던 컴포넌트에게 알림
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 되죠.
정리
정리하자면 이렇습니다. 기존에는 부모에서 자식의 자식의 자식까지 상태가 흘렀었는데, 리덕스를 사용하면 스토어를 사용하여 상태를 컴포넌트 구조의 바깥에 두고, 스토어를 중간자로 두고 상태를 업데이트 하거나, 새로운 상태를 전달받습니다. 따라서, 여러 컴포넌트를 거쳐서 받아올 필요 없이 아무리 깊숙한 컴포넌트에 있다 하더라도 직속 부모에게서 받아오는 것 처럼 원하는 상태값을 골라서 props 를 편리하게 받아올 수 있죠.
리액트 없이 쓰는 리덕스
리덕스는 리액트에 종속되는 그런 라이브러리가 아닙니다. 물론 리액트에서 쓰기위해 만든거니 궁합은 매우 잘맞죠! 한번, 평범한 HTML 와 JavaScript 환경에서 리덕스를 사용해가면서, 리덕스의 기본 개념을 배워봅시다.
<!DOCTYPE html><html><head><metacharset="utf-8"><metaname="viewport"content="width=device-width"><title>그냥 평범한 리덕스</title></head><body><h1id="number">0</h1><buttonid="increment">+</button><buttonid="decrement">-</button><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script></body></html>
그럼 이러한 결과가 만들어집니다.
자, 이제 자바스크립트를 작성 할 것입니다! 주석을 아주~ 상세하게 적어놓았으니, 주석도 함께 읽어가면서 자바스크립트를 작성해보세요.
// 편의를 위하여 각 DOM 엘리먼트에 대한 레퍼런스를 만들어줍니다.const elNumber =document.getElementById('number');const btnIncrement =document.getElementById('increment');const btnDecrement =document.getElementById('decrement');// 액션 타입을 정의해줍니다. const INCREMENT ='INCREMENT';const DECREMENT ='DECREMENT';// 액션 객체를 만들어주는 액션 생성 함수constincrement=(diff)=>({type: INCREMENT,diff: diff });constdecrement=()=>({type: DECREMENT });// 초기값을 설정합니다. 상태의 형태는 개발자 마음대로 입니다.const initialState ={number:0};/* 이것은 리듀서 함수입니다. state 와 action 을 파라미터로 받아옵니다. 그리고 그에 따라 다음 상태를 정의 한 다음에 반환해줍니다. */// 여기에 state = initialState 는, 파라미터의 기본값을 지정해줍니다.constcounter=(state = initialState, action)=>{console.log(action);switch(action.type){case INCREMENT:return{number: state.number + action.diff };case DECREMENT:return{number: state.number -1};default:return state;}}// 스토어를 만들 땐 createStore 에 리듀서 함수를 넣어서 호출합니다.const{ createStore }= Redux;const store =createStore(counter);// 상태가 변경 될 때 마다 호출시킬 listener 함수입니다constrender=()=>{ elNumber.innerText = store.getState().number;console.log('내가 실행됨');}// 스토어에 구독을하고, 뭔가 변화가 있다면, render 함수를 실행합니다. store.subscribe(render);// 초기렌더링을 위하여 직접 실행시켜줍니다.render();// 버튼에 이벤트를 달아줍니다.// 스토어에 변화를 일으키라고 할 때에는 dispatch 함수에 액션 객체를 넣어서 호출합니다. btnIncrement.addEventListener('click',()=>{ store.dispatch(increment(25));}) btnDecrement.addEventListener('click',()=>{ store.dispatch(decrement());})
이제 버튼들을 눌러보시면 숫자가 변경 될 것입니다. 지금까지 한 작업들을 정리해봅시다.
액션타입을 만들어주었습니다.
그리고 각 액션타입들을 위한 액션 생성 함수를 만들었습니다. 액션 함수를 만드는 이유는 그때 그때 액션을 만들 때마다 직접{ 이러한 객체 }형식으로 객체를 일일히 생성하는 것이 번거롭기 때문에 이를 함수화 한 것입니다. 나중에는 특히, 액션에 다양한 파라미터가 필요해 질 때 유용합니다.
변화를 일으켜주는 함수, 리듀서를 정의해주었습니다. 이 함수에서는 각 액션타입마다, 액션이 들어오면 어떠한 변화를 일으킬지 정의합니다. 지금의 경우에는 상태 객체에 number 라는 값이 들어져있습니다. 변화를 일으킬 때에는 불변성을 유지시켜주어야 합니다.
스토어를 만들었습니다. 스토어를 만들 땐 createStore 를 사용하며 만듭니다. createStore 에는 리듀서가 들어갑니다. (스토어의 초기상태, 그리고미들웨어도 넣을 수 있습니다.)
스토어에 변화가 생길 때 마다 실행시킬 리스너 함수 render 를 만들어주고,store.subscribe를 통하여 등록해주었습니다.
하나의 애플리케이션에선 단 한개의 스토어를 만들어서 사용합니다. 사실, 권장되지는 않습니다. 여러개의 스토어를 만들고 싶다면 만들 수는 있습니다. 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 그렇게 여러개의 스토어를 만들 수도 있습니다. 하지만 그렇게 하면, 개발 도구를 활용하지 못하게 됩니다.
2. 상태는 읽기전용 입니다.
리액트에서 state 를 업데이트 해야 할 때, setState 를 사용하고, 배열을 업데이트 해야 할 때는 배열 자체에 push 를 직접 하지 않고, concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트를 합니다. 엄청 깊은 구조로 되어있는 객체를 업데이트를 할 때도 마찬가지로, 기존의 객체는 건들이지 않고Object.assign을 사용하거나 spread 연산자 (...) 를 사용하여 업데이트 하곤 하죠.
리덕스에서도 마찬가지입니다. 기존의 상태는 건들이지 않고 새로운 상태를 생성하여 업데이트 해주는 방식으로 해주면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있습니다.
리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여shallow equality검사를 하기 때문입니다. 이를 통하여 객체의 변화를 감지 할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있는 것이죠.
이전의 상태는 절대로 건들이지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환합니다.
똑같은 파라미터로 호출된 리듀서 함수는언제나똑같은 결과값을 반환해야만 합니다.
3가지 사항을 주의해주세요. 동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 합니다. 그런데 일부 로직들 중에서는 실행 할 때마다 다른 결과값이 나타날 수도 있죠. new Date() 를 사용한다던지… 랜덤 숫자를 생성한다던지… 혹은, 네트워크에 요청을 한다던지! 그러한 작업은 결코 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해줘야 합니다. 그런것을 하기 위해서,리덕스 미들웨어를 사용하곤 하죠.
리액트에서 리덕스 사용하기
이번엔 리액트에서 리덕스를 사용하는 방법에 대해서 배워보겠습니다. 이 튜토리얼에서는, 제목에서 나타나 있듯이 ‘리덕스를 편하게 사용하기 위한 발악’ 을 해볼거에요.리덕스 매뉴얼에서도 나타나는 정석대로만 한다면, 액션을 위한 파일과 리듀서를 위한 파일이 따로따로 구분되어있습니다. 정석대로만 하는 방법을 배우고 싶으시다면카운터 만들기튜토리얼을 읽어보세요.
Root은 우리 프로젝트의 최상위 컴포넌트 입니다. CounterContainer 와 TodosContainer.js 는 현재 비어있는 컴포넌트들인데 이 컴포넌트들은 나중에 우리가 리덕스와 연동을 해 줄 컴포넌트입니다.
리덕스와 연동된 컴포넌트를 우리는컨테이너 컴포넌트라고 부릅니다. 반면, 단순히 뷰 만을 보여주기 위하여 만들어진 컴포넌트는프리젠테이셔널 컴포넌트라고 부릅니다. 리덕스를 사용하여 만든 리액트 애플리케이션에서는 대부분 이렇게 컴포넌트를 구분합니다. 이러한 패턴은 무조건 따라야 하는 것은 아니지만, 이렇게 하면 앞으로 프로젝트를 개발 할 때 매우 편해집니다.
4. 리덕스 관련 코드를 작성 할 파일 생성
다음 파일들은 리덕스 관련 코드를 작성하기 위하여 필요한 파일들입니다.
store
modules
counter.js
todo.js
index.js
configure.js
index.js
actionCreators.js
이 파일들은 현재 다 비어있으며 우리가 앞으로 채워나갈것입니다.
우리는 액션과 리듀서를 기능별로 분류하여 하나의 파일에 작성하게 되는데 이를 module 이라고 부릅니다. 예를들어 카운터에 관련된 코드는 counter.js 에서 작성하고, 투두리스트에 관련된건 todo.js 에 작성하게 되죠.
그리고, configure.js 는 리덕스 스토어를 생성하는 함수를 모듈화하여 내보냅니다. 이렇게 따로 모듈화를 하는 이유는, 하나의 애플리케이션에서는 하나의 스토어밖에 없긴 하지만 예외의 케이스가 있기 때문입니다. 나중에 여러분이 서버사이드 렌더링을 하게 된다면, 서버쪽에서도 각 요청이 처리 될 때마다 스토어를 생성해주어야 하는데요, 그런 작업을 하게 될 때 이렇게 스토어를 생성하는 함수를 이렇게 모듈화 하곤 합니다.
그리고, store/index.js 에선 스토어를 생성한다음에 내보내줍니다. 이렇게 모듈화된 스토어는 브라우저쪽에서만 사용되는 스토어입니다 (서버사이드 렌더링을 하게 될 땐 아까 언급했던 configure 를 통하여 그때 그때 만듭니다). 이렇게 모듈화된 스토어는 리액트 애플리케이션을 초기설정 할 때 사용됩니다.
actionCreators.js 에서도 스토어를 불러오고, 또 각 모듈들에서 선언 했던 액션 생성함수들을 불러와서 store 의 dispatch 와 미리 바인딩 작업을 해줍니다. (이 부분은 나중에 다루겠습니다.)
프로젝트의 각 파일들을 열어가면서 각 파일들이 어떠한 역할을 하는지 한번 살펴보세요.
카운터 구현하기
카운터의 상태를 리덕스를 사용하여 관리해보겠습니다. 구현하기에 앞서, Counter.js 컴포넌트를 살펴봅시다.
이 컴포넌트에서는, 숫자값 number 와, 값을 증가시키는 함수 onIncrement, 그리고 값을 감소시키는 함수 onDecrement 를 props 로 받아옵니다.
counter 모듈 작성하기
그럼, 여기서 필요한 리덕스 모듈을 작성해봅시다.
src/store/modules/counter.js// 액션 타입을 정의해줍니다.const INCREMENT ='counter/INCREMENT';const DECREMENT ='counter/DECREMENT';// 액션 생성 함수를 만듭니다.// 이 함수들은 나중에 다른 파일에서 불러와야 하므로 내보내줍니다.exportconstincrement=()=>({type: INCREMENT });exportconstdecrement=()=>({type: DECREMENT });// 모듈의 초기 상태를 정의합니다.const initialState ={number:0};// 리듀서를 만들어서 내보내줍니다.exportdefaultfunctionreducer(state = initialState, action){// 리듀서 함수에서는 액션의 타입에 따라 변화된 상태를 정의하여 반환합니다.// state = initialState 이렇게 하면 initialState 가 기본 값으로 사용됩니다.switch(action.type){case INCREMENT:return{number: state.number +1};case DECREMENT:return{number: state.number -1};default:return state;// 아무 일도 일어나지 않으면 현재 상태를 그대로 반환합니다.}}
리덕스 매뉴얼에선 액션과 리듀서를 각각 다른 파일에 작성하여 관리하는 것을 알려주는데요, 그렇게 사용 했을때는, 새 액션을 추가 할 때마다 두개의 파일을 건들여야 한다는점이 불편합니다. 이렇게 하나의 파일에 모두 작성하는 것은Ducks구조라고 부릅니다.
이 구조에서는, 리덕스 관련 코드를 기능별로 하나의 파일로 나눠서 작성합니다. 액션이름을 만들 때에는 const 를 사용하여 레퍼런스에 문자열을 담는데, 앞에 도메인을 추가하는 방식으로, 서로 다른 모듈에서 동일한 액션 이름을 가질 수 있게 됩니다. 예를들어서, 다른 모듈에서도 INCREMENT 라는 이름을 사용하되 “another/INCREMENT” 값을 담게 하면 되겠죠?
redux-actions 의 createAction 과 handleActions 사용하기
위 코드에서는 각 액션들마다 액션 객체를 만들어주는 액션 생성 함수를 일일히 작성해주었습니다. redux-actions 의 createAction 이라는 함수를 사용하면 액션 생성 함수 코드를 다음과 같이 작성 할 수 있게 됩니다.
src/store/modules/counter.jsimport{ createAction }from'redux-actions';// 액션 타입을 정의해줍니다.const INCREMENT ='counter/INCREMENT';const DECREMENT ='counter/DECREMENT';// 액션 생성 함수를 만듭니다.exportconst increment =createAction(INCREMENT);exportconst decrement =createAction(DECREMENT);(...)
액션 생성함수에서 파라미터를 필요하게 되는 경우에도 createAction 을 사용 할 수 있습니다. 그 부분은 추후 우리가 todo 모듈을 작성하게 될 때 알아보겠습니다.
우리가 기존에 작성한 리듀서에서는 각 액션타입에 따라 다른 작업을 하기 위해서 switch 구문을 사용했었죠? switch 문은 block 으로 따로 나뉘어져 있는것이 아니기 때문에 이러한 작업은 못합니다:
switch(value){case0:const a =1;break;case1:const a =2;// ERROR!break;default:}
그 이유는, const 혹은 let 의 스코프는 블록({ }) 으로 제한되어있는데, 모든 case 는 하나의 블록안에 있기 때문에, 위와같이 중복 선언이 불가능해진다는 문제점도 있고, 여러모로 switch case 문은 귀찮습니다.
handleActions 를 사용하면, 리듀서 코드를 조금 더 깔끔하게 작성 할 수 있습니다.
src/store/modules/counter.jsimport{ createAction, handleActions }from'redux-actions';// 액션 타입을 정의해줍니다.const INCREMENT ='counter/INCREMENT';const DECREMENT ='counter/DECREMENT';// 액션 생성 함수를 만듭니다.exportconst increment =createAction(INCREMENT);exportconst decrement =createAction(DECREMENT);// 모듈의 초기 상태를 정의합니다.const initialState ={number:0};// handleActions 의 첫번째 파라미터는 액션을 처리하는 함수들로 이뤄진 객체이고// 두번째 파라미터는 초기 상태입니다.exportdefaulthandleActions({[INCREMENT]:(state, action)=>{return{number: state.number +1};},// action 객체를 참조하지 않으니까 이렇게 생략을 할 수도 있겠죠?// state 부분에서 비구조화 할당도 해주어서 코드를 더욱 간소화시켰습니다.[DECREMENT]:({ number })=>({number: number -1})}, initialState);
combineReducers 로 리듀서 합치기
지금은 리듀서가 하나밖에 없지만, 앞으로 우리가 todo 리듀서도 만들고 나면 한 프로젝트에 여러개의 리듀서가 존재하게 됩니다. 여러개의 리듀서가 있을 때에는, redux 의 함수 combineReducers 를 사용하여 하나의 리듀서로 합쳐줄 수 있습니다. 이렇게 합쳐진 리듀서는루트 리듀서라고 부릅니다.
컴포넌트가 그대로 렌더링되고 있나요? 그러면 CounterContainer 를 리덕스에 연결해주겠습니다. 코드의 하단부 부터 주석과 함께 코드를 읽어가면서 작성해보세요.
src/containers/CounterContainer.jsimport React,{ Component }from'react';import Counter from'components/Counter';import{ connect }from'react-redux';import*as counterActions from'store/modules/counter';classCounterContainerextendsComponent{handleIncrement=()=>{this.props.increment();}handleDecrement=()=>{this.props.decrement();}render(){const{ handleIncrement, handleDecrement }=this;const{ number }=this.props;return(<CounteronIncrement={handleIncrement}onDecrement={handleDecrement}number={number}/>);}}// props 값으로 넣어 줄 상태를 정의해줍니다.constmapStateToProps=(state)=>({ number: state.counter.number });// props 값으로 넣어 줄 액션 함수들을 정의해줍니다constmapDispatchToProps=(dispatch)=>({ increment:()=>dispatch(counterActions.increment()), decrement:()=>dispatch(counterActions.decrement())})// 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다.// connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다.// 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다.exportdefaultconnect(mapStateToProps, mapDispatchToProps)(CounterContainer);
mapStateToProps 스토어의 상태를 파라미터로 받아오는 함수로서, 컴포넌트에 상태로 넣어줄 props 를 반환합니다. mapDispatchToProps 는 dispatch 를 파라미터로 받아오는 함수로서, 컴포넌트에 넣어줄 액션 함수들을 반환합니다.
코드를 저장하고 카운터의 증가버튼와 감소버튼을 눌러보세요. 숫자가 바뀌나요?
보통은 위와 같은 코드처럼, mapStateToProps 와 mapDispatchToProps 를 따로 만들곤 하는데, 사람마다 차이가 있을 수 있겠지만 그냥 함수를 connect 내부에서 정의하면 코드가 조금 더 깔끔해집니다.
코드가 좀 간소화됐죠? 나중에 가면 여러분이 만들 컨테이너 컴포넌트에서 여러 모듈에서 액션 생성 함수를 참조해야 하게 되는 일도 있습니다. 그러한 경우엔 다음과 같이 bindActionCreators 의 결과물을 CounterActions 라는 props 로 넣어주면 됩니다. 그리고 물론, 이에 따라 메소드들도 조금 바꿔줘야겠죠?
우리는 상태를 업데이트 할 때, 기존의 객체는 건들이지 않고 새 객체를 만들어주어야 합니다. 즉, 불변성을 유지해가면서 상태를 업데이트해야한다는 것이죠. 다음 예제를 한번 읽어보세요.
let nextState =null;// input 값을 바꾼 새 객체를 만들기 nextState ={...state,input:'새로운 값'};// todos 에 항목 추가하기 nextState ={...state,todos: state.todos.concat({id:2,text:'새로운거',checked:false})};// 0번째 항목 checked 값 반전하기const nextTodos =[...state.todos]; nextTodos[0]={...nextTodos[0],checked:!nextTodos.checked }; nextState ={...state,todos: nextTodos }
일반 자바스크립트를 사용하여 불변성을 유지해가면서 상태를 업데이트하는것은 그렇게 어려운 작업은 아니지만, 귀찮은 것은 사실이며, 조금 더 많은 코드를 작성해야하는 것 또한 사실입니다. Immutable.js 를 사용하면 위 코드들은 다음과 같이 간단하게 바뀔 수 있습니다.
let nextState =null;// input 값을 바꾼 새 객체를 만들기 nextState = state.set('input','새로운 값');// todos 에 항목 추가하기 nextState = state.update('todos', todos => todos.push(Map({id:2,text:'새로운거',checked:false})));// 0번째 항목 checked 값 반전하기 nextState = state.updateIn(['todos',0,'checked'], checked =>!checked);
Immutable.js 를 꼭 써야하는 것은 아닙니다만, 익숙해지면 개발이 매우 편해집니다. 만약에 Immutable.js 가 익숙하지 않다면관련 포스트를 꼭 한번 읽고 오세요!
todo.js 모듈 작성하기
그럼, 투두리스트를 위한 todo.js 모듈을 작성해봅시다! 먼저 액션 생성함수들을 작성해볼까요?
우리가 이번에 만든 액션함수들은, 참조해야 할 값들이 필요합니다. 예를들어서, changeInput 은 다음 어떤 값으로 바뀌어야 할지를 알려주는 값이 필요하고, insert 는 추가 할 내용, 그리고, toggle 과 remove 는 어떤 id 를 수정해야 할 지 알려주어야겠죠.
createAction 을 통하여 만든 액션생성함수에 파라미터를 넣어서 호출하면, 자동으로 payload 라는 이름으로 통일되어 설정됩니다.
가끔씩은 여러종류의 값을 전달해야 될 때도 있겠죠. 그럴 땐 이렇게 객체를 넣어주면 됩니다.
const multi =createAction('MULTI');multi({foo:1,bar:2});// { type: 'MULTI', payload: { foo: 1, bar: 2 } }
그런데, 코드상에서 해당 액션함수들이 어떠한 파라미터를 받는지 명시하고 싶을 수도 있습니다. createAction 함수는 세가지의 파라미터를 받는데요, 첫번째는 액션이름, 두번째는 payloadCreator, 세번째는 metaCreator 입니다. 두번째와 세번째 파라미터는 payload 값과 meta 값을 지정해주는 함수인데요, 다음 코드를 보면 이해하기 쉽습니다.
예제:
const sample =createAction('SAMPLE',(value)=> value +1,(value)=> value -1);sample(1);// { type: 'SAMPLE', payload: 2, meta: 0 }
payloadCreator 가 생략되어있을때는, 액션생성함수의 파라미터가 그대로 payload 값으로 설정되며, metaCreator 가 생략되어있을때에는, meta 값을 따로 생성하지 않습니다.
따라서, 우리가 작성한 코드는 다음과 같이 수정 할 수 있습니다.
src/store/modules/todo.jsimport{ createAction }from'redux-actions';const CHANGE_INPUT ='todo/CHANGE_INPUT';const INSERT ='todo/INSERT';const TOGGLE ='todo/TOGGLE';const REMOVE ='todo/REMOVE';exportconst changeInput =createAction(CHANGE_INPUT, value => value);exportconst insert =createAction(INSERT, text => text);exportconst toggle =createAction(TOGGLE, id => id);exportconst remove =createAction(REMOVE, id => id);
이렇게 하면, 위 액션 생성함수들이 어떠한 값을 파라미터로 받는지 알겠지요?
자, 그럼 이어서 초기상태를 정의하고, 리듀서 함수도 작성해보겠습니다.
src/store/modules/todo.jsimport{ createAction, handleActions }from'redux-actions';import{Map, List }from'immutable';const CHANGE_INPUT ='todo/CHANGE_INPUT';const INSERT ='todo/INSERT';const TOGGLE ='todo/TOGGLE';const REMOVE ='todo/REMOVE';exportconst changeInput =createAction(CHANGE_INPUT, value => value);exportconst insert =createAction(INSERT, text => text);exportconst toggle =createAction(TOGGLE, id => id);exportconst remove =createAction(REMOVE, id => id);let id =0;// todo 아이템에 들어갈 고유 값 입니다const initialState =Map({input:'',todos:List()});exportdefaulthandleActions({// 한줄짜리 코드로 반환 할 수 있는 경우엔 다음과 같이 블록 { } 를 생략 할 수 있습니다.[CHANGE_INPUT]:(state, action)=> state.set('input', action.payload),[INSERT]:(state,{ payload: text })=>{// 위 코드는 action 객체를 비구조화 할당하고, payload 값을 text 라고 부르겠다는 의미입니다.const item =Map({id: id++,checked:false, text });// 하나 추가 할 때마다 id 값을 증가시킵니다.return state.update('todos', todos => todos.push(item));},[TOGGLE]:(state,{ payload: id })=>{// id 값을 가진 index 를 찾아서 checked 값을 반전시킵니다const index = state.get('todos').findIndex(item=> item.get('id')=== id);return state.updateIn(['todos', index,'checked'], checked =>!checked);},[REMOVE]:(state,{ payload: id })=>{// id 값을 가진 index 를 찾아서 지웁니다.const index = state.get('todos').findIndex(item=> item.get('id')=== id);return state.deleteIn(['todos', index]);}}, initialState);
Immutable.js 를 쓰지 않았다면, 아마 코드의 양은 1.5배 정도 많아졌을 것입니다. 더군다나, handleAction 을 쓰지 않았다면 위와 같이 action 비구조화 할당을 하거나, 한줄로 처리 할 방법도 없었겠죠.
새로운 모듈을 다 만들었다면, combineReducers 안에 넣어주어야합니다.
src/store/modules/todo.jsimport{ combineReducers }from'redux';import counter from'./counter';import todo from'./todo';exportdefaultcombineReducers({ counter, todo });
TodoContainer 컴포넌트 작성하기
우리가 CounterContainer 에 했던것과 동일한 작업을 진행해주겠습니다. TodosContainer 에서 Todos 를 불러와서 렌더링 하고, 기존에 App 에서 Todos 가 들어가던 자리를 TodosContainer 로 대체해주는 것이죠.
그냥 전형적인 투두리스트입니다. 할일목록이 들어있는 todos 값과, 인풋 내용 input 값을 받아옵니다. 그리고 4가지 함수도 props 로 받아오죠.
onInsert: 추가 (버튼 클릭 시)
onToggle: 삭제선 켜고 끄기 (아이템 클릭 시)
onRemove: 제거 (아이템 더블 클릭 시)
onChange: 인풋 값 수정
props 로 받아온 todos 는 Immutable List 형태입니다. Immutable List 는 완전한 배열은 아니지만, 리액트에서 호환이 되기 때문에 map 함수를 사용하여 컴포넌트 List 를 렌더링 했을 때 오류 없이 렌더링 할 수 있습니다. 추가적으로, List 안에 들어있는 것들은 Map 이므로, 내부 아이템들을 조회 할 때에는 .get() 을 사용하거나, .toJS() 를 통하여 일반 객체로 변환 후 사용해주어야 합니다.
자, 그러면 TodosContainer 를 본격적으로 구현해봅시다! 주석을 하나 하나 잘 읽어주세요.
src/containers/TodosContainer.jsimport React,{ Component }from'react';import Todos from'components/Todos';import{ connect }from'react-redux';import{ bindActionCreators }from'redux';import*as todoActions from'store/modules/todo';classTodosContainerextendsComponent{handleChange=(e)=>{// 인풋 값 변경const{ TodoActions }=this.props; TodoActions.changeInput(e.target.value);}handleInsert=()=>{// 아이템 추가const{ input, TodoActions }=this.props; TodoActions.insert(input);// 추가하고 TodoActions.changeInput('');// 기존 인풋값 비우기}handleToggle=(id)=>{// 삭제선 켜고 끄기const{ TodoActions }=this.props; TodoActions.toggle(id);}handleRemove=(id)=>{// 아이템 제거const{ TodoActions }=this.props; TodoActions.remove(id);}render(){const{ handleChange, handleInsert, handleToggle, handleRemove }=this;const{ input, todos }=this.props;return(<Todosinput={input}todos={todos}onChange={handleChange}onInsert={handleInsert}onToggle={handleToggle}onRemove={handleRemove}/>);}}exportdefaultconnect(// state 를 비구조화 할당 해주었습니다({ todo })=>({// immutable 을 사용하니, 값을 조회 할 때엔느 .get 을 사용해주어야하죠. input: todo.get('input'), todos: todo.get('todos')}),(dispatch)=>({ TodoActions:bindActionCreators(todoActions, dispatch)}))(TodosContainer);
이제 투두리스트에서 인풋을 수정해보고, 버튼을 클릭해서 새 투두아이템을 생성해보세요. 그리고 생성된 아이템을 클릭하여 삭제선을 껐다 켜보시고, 더블클릭하여 제거해보세요.
엑스트라…
축하합니다! 우리가 구현해야 할 주요 작업들은 모두 끝났습니다. 하지만, 이 튜토리얼은 완전히 끝나지는 않았습니다. 앞으로 해결해야 할 항목이 두가지 남았습니다. 근데 이것들은 꼭 해야 하는 것은 아닙니다. 다만, 개발을 어쩌면 조금 더 편하게 해줄 수 는 있습니다.
액션생성함수를 미리 bind 하기
리덕스를 사용하면서 의문점이 들었습니다. 지금의 경우엔, CounterContainer 에서 counter 모듈의 액션생성함수를 참조하고, TodosContainer 에서 todo 모듈의 액션생성함수를 참조하고 있는데요, 실제 프로젝트에서는 한 종류의 모듈을 여러곳에서 사용 할 일이 많습니다. 예를들어서, form 이라는 모듈에서 폼 만을 관리하는 모듈을 만들 수도 있는 것이고, modal 이라는 모듈을 만들어서 모든 모달들을 관리 할 수도 있고… 또 header 라는 모듈을 만들어서 헤더에 관련된 액션들을 관리 하게 될 수도 있죠.
그런데, 그러한 액션들을 사용 할 때마다 mapDispatchToProps 에 해당하는 부분을 계속 작성하는 것이 저는 굉장히 귀찮다고 생각했습니다. 그래서, 최근 액션생성함수를 미리 bind 하는 것을 시도해봤는데, 꽤 만족스러웠어서 여기에도 소개 해볼까 합니다.
일단, 이것을 하기 위해선, 리덕스 스토어 인스턴스가 모듈화되어 불러 올 수 있는 상태여야 합니다. (우리는 이미 그렇게 했죠) 참고로 리덕스 매뉴얼에서FAQ란을 보면 스토어 인스턴스를 모듈화하여 내보내는것을 권장하고 있지 않다고 적혀있는데 그 이유는 나중에 리덕스 앱을 분리시킬때 힘들것이기 때문이라고 적어놓았는데요, 여러개의 컴포넌트에서 스토어를 직접 불러와서 접근하는 것이 아니기 때문에 이 부분은 문제되지 않습니다.
src/containers/CounterContainer.jsimport React,{ Component }from'react';import Counter from'components/Counter';import{ connect }from'react-redux';import{ bindActionCreators }from'redux';import{ CounterActions }from'store/actionCreators';classCounterContainerextendsComponent{handleIncrement=()=>{ CounterActions.increment();}handleDecrement=()=>{ CounterActions.decrement();}render(){const{ handleIncrement, handleDecrement }=this;const{ number }=this.props;return(<CounteronIncrement={handleIncrement}onDecrement={handleDecrement}number={number}/>);}}/* 첫번째 파라미터 mapStateToProps: props 값으로 넣어 줄 상태를 정의해줍니다. 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다. connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다. 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다. */exportdefaultconnect((state)=>({ number: state.counter.number }))(CounterContainer);
아직 이 방법은 실험적입니다. 무조건 이렇게 하라고 권장하지는 않겠습니다 🙂 하지만, 여러분들이 만약에 앞으로 작업을 하면서 mapDispatchToProps 를 일일히 하는것이 귀찮아진다고 느낄때면, 이러한 방법이 있다는 것을 참고하세요~
조회 할 때 .get 하는 것이 맘에 안든다! 그렇다면 Record 사용하기
Immutable.js 를 사용해서 상태를 업데이트하는것은 정말로 편합니다. 하지만, 값을 조회 할 때 마다 .get 을 사용해야 한다는 것은 조금 귀찮을 수도 있는데요, 만약 Map 대신 Record 를 사용하게 된다면 이 부분이 해결됩니다. Record 를 사용하면, Map 을 다룰때와 똑같이 사용 할 수 있는데 차이점은, state.input, state.todos 이런식으로 직접 조회 할수 있게 됩니다.
todo 모듈을 다음과 같이 수정해주세요.
src/store/modules/todo.jsimport{ createAction, handleActions }from'redux-actions';import{ Record, List }from'immutable';const CHANGE_INPUT ='todo/CHANGE_INPUT';const INSERT ='todo/INSERT';const TOGGLE ='todo/TOGGLE';const REMOVE ='todo/REMOVE';exportconst changeInput =createAction(CHANGE_INPUT, value => value);exportconst insert =createAction(INSERT, text => text);exportconst toggle =createAction(TOGGLE, id => id);exportconst remove =createAction(REMOVE, id => id);let id =0;// todo 아이템에 들어갈 고유 값 입니다// Record 함수는 Record 형태 데이터를 만드는 함수를 반환합니다.// 따라서, 만든 다음에 뒤에 () 를 붙여줘야 데이터가 생성됩니다.const initialState =Record({input:'',todos:List()})();// Todo 아이템의 형식을 정합니다.const TodoRecord =Record({id: id++,text:'',checked:false})exportdefaulthandleActions({[CHANGE_INPUT]:(state, action)=> state.set('input', action.payload),[INSERT]:(state,{ payload: text })=>{// TodoRecord 를 사용해야 아이템도 Record 형식으로 조회 가능합니다. // 빠져있는 값은, 기본값을 사용하게 됩니다 (checked: false)const item =TodoRecord({id: id++, text });return state.update('todos', todos => todos.push(item));},[TOGGLE]:(state,{ payload: id })=>{const index = state.get('todos').findIndex(item=> item.get('id')=== id);return state.updateIn(['todos', index,'checked'], checked =>!checked);},[REMOVE]:(state,{ payload: id })=>{const index = state.get('todos').findIndex(item=> item.get('id')=== id);return state.deleteIn(['todos', index]);}}, initialState);
그러면, 이에 따라 TodosContainer 를 다음과 같이 수정해도 되겠죠?
src/containers/TodosContainer.jsimport React,{ Component }from'react';import Todos from'components/Todos';import{ connect }from'react-redux';import{ bindActionCreators }from'redux';import*as todoActions from'store/modules/todo';classTodosContainerextendsComponent{(...)}exportdefaultconnect(({ todo })=>({// 일반 객체 다루듯이 다루면 됩니다. input: todo.input,todos: todo.todos }),(dispatch)=>({TodoActions:bindActionCreators(todoActions, dispatch)}))(TodosContainer);
추가적으로, 내부에 있는 아이템들도 Record 형태이기 때문에, Todos 컴포넌트에서 .toJS() 도 생략해줘도 됩니다.
src/components/Todos.js– 컴포넌트로 map 하는 부분 const todoItems = todos.map(todo=>{const{ id, checked, text }= todo;return(<TodoItemid={id}checked={checked}text={text}onToggle={onToggle}onRemove={onRemove}key={id}/>)})
Record 를 쓰면, .get, .getIn 이런걸 쓰지 않아도 되기 때문에 편리한점이 많습니다. 하지만 그 대신에 제한도 조금 생깁니다. 예를들어서, 다음과 같은 코드는 제대로 작동하지 않습니다.
const HumanRecord =Record({name:'John',age:10});let human =HumanRecord(); human = human.set('job','developer');// Error: Cannot set unknown key "job" on n
Record 를 사용하면, 초반에 Record 에 정의한 값만 설정 할 수 있습니다. 때문에, 데이터가 지니고 있는 key 가 유동적이라면, 필요한 부분에 Map 을 사용하는 것이 옳은 선택입니다.
정리
이번 튜토리얼을 통하여, 리덕스를 프로젝트에서 사용하는 방법을 배워보았습니다. 많은 사람들이 초반에 리덕스를 사용 할 때 어려워 하기도 하는데, 몇 번 사용하고나면 굉장히 간단하다는 것을 느끼게 되고, 상태관리를 정말 편하게 해준다는 것을 깨닫게 됩니다. 물론, 지금과 같이 단순히 카운터, 투두리스트 같은것은 리액트 state 를 사용하는 것이 훨씬 쉬운것은 사실입니다. 하지만 나중에 상태와 업데이트 방식의 종류가 많아진다면, 리덕스가 없으면 정말 관리하기 복잡해집니다.
코드 기반에 있는 모든 코드는 마치 한 사람이 작성한 것처럼 보여야 합니다. 많은 사람이 코드 작성에 참여했더라도 말이죠.
제가 원저자인 프로젝트의 코드를 작성할 때는 몇 가지 원칙을 따르는데요, 아래의 목록에 그 원칙들을 나열하였습니다. 이 프로젝트의 코드작성에 참여할 때에는 다음의 가이드라인을 따라야 합니다.
다른 사람들에게 제 코드 작성 스타일을 따르라고 강요하려는 의도는 아닙니다. 이미 준수하는 코드작성 스타일이 있다면, 그것을 따라야 하겠지요.
"스타일에 관한 논쟁은 무의미하다. 스타일 가이드가 있을테니, 거기에 따르기만 해라"
RebeccaMurphey
"성공적인 프로젝트의 멋진 일원이 되기 위해서는 여러분 마음대로 코드를 작성하는 것이 나쁜 생각™임을 깨닫는 것이지요. 수천만 사람들이 여러분의 코드를 사용한다면, 가장 명확하게 코딩해야 합니다. 언어 명세가 허용하는 한 가장 똑똑한 코드를 만들겠다는 당신의 개인적인 선호에 따라서가 아니라"
이 글의 섹션은 현대적인 자바스크립트 개발을 위한합리적인스타일가이드를 제시하고 있지만, 그게 규정이라는 의미는 아닙니다. 제일 중요한 것은일관성 있는 코딩 스타일을 위한 규칙입니다. 프로젝트에 어떤 스타일을 사용하던간에 반드시 지켜져야 합니다. 이 문서를 프로젝트의 일관성, 가독성, 유지보수용이성을 위한 참고자료로써 링크해 주세요.
// if나 else, for while, try를 쓸 때에는 항상 빈 칸을 띠우고, 괄호를 사용하고, 여러 줄로 나누어 쓰세요.// 이렇게 하면 가독성이 더 좋아집니다.// 2.A.1.1// 빼곡해서 알아보기 어려운 구문의 예if(condition) doSomething();
while(condition) iterating++;
for(var i=0;i<100;i++) someIterativeFn();
// 2.A.1.1// 가독성이 높아지도록 빈 칸을 띠워주세요.if ( condition ) {
// 코드
}
while ( condition ) {
// 코드
}
for ( var i =0; i <100; i++ ) {
// 코드
}
// 아래처럼 하면 더 좋습니다:var i,
length =100;
for ( i =0; i < length; i++ ) {
// 코드
}
// 아니면 이렇게 할 수도 있죠...var i =0,
length =100;
for ( ; i < length; i++ ) {
// 코드
}
var prop;
for ( prop in object ) {
// 코드
}
if ( true ) {
// 코드
} else {
// 코드
}
B. 할당, 선언, 함수(일반, 표현식, 생성자)
// 2.B.1.1// 변수var foo ="bar",
num =1,
undef;
// 리터럴 표기법:var array = [],
object = {};
// 2.B.1.2// 함수 같은 유효 범위에 `var` 를 하나만 사용하면 가독성이 높아집니다.// 이렇게 하면 선언 목록도 깔끔해집니다(아울러 키보드로 입력해야 할 양도 줄어들지요)// 나쁜 스타일var foo ="";
var bar ="";
var qux;
// 좋은 스타일var foo ="",
bar ="",
quux;
// 또는..var// 아래 변수에 대한 설명
foo ="",
bar ="",
quux;
// 2.B.1.3// var 문은 관련있는 스코프(함수) 안의 시작하는 곳에 있어야합니다.// ECMAScript 6의 const, let도 같은 맥락으로 이 규칙이 적용됩니다.// 나쁜 스타일functionfoo() {
// 여기에 어떤 구문이 있음var bar ="",
qux;
}
// 좋은 스타일functionfoo() {
var bar ="",
qux;
// 변수 선언이후에 모든 구문들이 옴.
}
// 2.B.2.1// 일반 함수 선언functionfoo( arg1, argN ) {
}
// 사용법foo( arg1, argN );
// 2.B.2.2// 일반 함수 선언functionsquare( number ) {
return number * number;
}
// 사용법square( 10 );
// 아주 부자연스러운 연속 전달 스타일(continuation passing style) (CPS에 대해서는 http://goo.gl/TA32o를 참고)functionsquare( number, callback ) {
callback( number * number );
}
square( 10, function( square ) {
// 콜백 문장
});
// 2.B.2.3// 함수 표현식varsquare=function( number ) {
// 가치 있고 의미 있는 뭔가를 반환합니다return number * number;
};
// 식별자를 지닌 함수 표현식// 아래와 같은 형태는 자기 자신을 호출할 수 있으면서// 스택 트레이스상에서 식별할 수 있다는 부가적인 장점이 있습니다:varfactorial=functionfactorial( number ) {
if ( number <2 ) {
return1;
}
return number *factorial( number-1 );
};
// 2.B.2.4// 생성자 선언functionFooBar( options ) {
this.options= options;
}
// 사용법var fooBar =newFooBar({ a:"alpha" });
fooBar.options;
// { a: "alpha" }
C. 예외 사항, 약간의 탈선
// 2.C.1.1// 콜백을 포함한 함수foo(function() {
// 참고로 함수 호출을 실행하는 첫 괄호와 "function"이라는// 단어 사이에는 별도의 공백이 없습니다.
});
// 배열을 받는 함수일 때에는 공백 없음foo([ "alpha", "beta" ]);
// 2.C.1.2// 객체를 받는 함수일 때에는 공백 없음foo({
a:"alpha",
b:"beta"
});
// 괄호안에 괄호가 있을 때에는 공백 없음if ( !("foo"in obj) ) {
}
D. 일관성을 지키는 편이 항상 좋습니다.
섹션2.A-2.C에서는 우선 공백을 사용하는 규칙을 제시하고 있습니다. 공백을 사용하는 원칙은 단순하면서도 우선순위가 높습니다. 바로 일관성이에요. "inner whitespace"를 쓰라는 등의 포맷방식을 모두에게 강요할 수는 없겠지요. 그러나 여러분의 프로젝트 전체에 걸쳐서 단 한 가지의 스타일이 적용되어야 합니다. 이 점이 중요합니다.
// 2.D.1.1if (조건) {
// 코드
}
while (조건) {
// 코드
}
for (var i =0; i <100; i++) {
// 코드
}
if (true) {
// 코드
} else {
// 코드
}
E. 따옴표
작은 따옴표로 하냐 큰 따옴표로 하냐는 별로 중요하지않습니다, 자바스크립트에서는 그둘은 파싱하는 과정에서 전혀 차이가 없으니까요. 중요한것은절대적인 일관성이 있어야 한다는 점입니다.같은 프로젝트에서는 절대로 큰따옴표와 작은따옴표를 섞지 마세요. 한 스타일을 정해서 고수하세요.
F. 각 줄의 끝부분, 빈 줄
여백이 있으면 바뀐 내용들을 읽을 수가 없게 됩니다. 또한 diff툴이 제대로 작동하지 않습니다. 줄의 끝에 있는 여백이나 빈 줄에 들어있는 공백을 자동적으로 제거해주는 ‘pre-commit hook'을 사용해 보세요.
object.prop === undefined
object.hasOwnProperty( prop )
"prop" in object
B. 강제변환된 타입
다음의 결과를 생각해 보죠...
아래와 같은 HTML 코드가 있어요:
<inputtype="text"id="foo-input"value="1">
// 3.B.1.1// `foo` 라는 변수를 `0` 값을 갖는 상태로 선언하였습니다. 그리고 자료형은 `수(number)` 이고요.var foo =0;
// typeof foo;// "number"...// 나중에 입력요소(input element)에서 받은 새로운 값을// `foo` 변수에다가 넣어야 합니다.
foo =document.getElementById("foo-input").value;
// (역자주 : 아, `foo` 변수에 원래는 숫자형 자료가 들어가 있었는데, 이번에는 문자형 자료가 들어가네요.)// 자, 이제 `typeof foo` 라고 쳐서 `foo` 의 자료형을 확인해보면, 결과는 `문자열` 이// 나올 겁니다. 즉 `foo` 의 자료형을 알아볼 때, 아래와 같이 했다면:if ( foo ===1 ) {
importantTask();
}
// `importantTask()` 함수는 절대로 실행이 안될 겁니다. `foo` 의 값이 "1" 이라고 하더라도 말이죠.// 3.B.1.2// 1진법의 +나 - 단항 연산자를 사용한 smart 강제변환으로 문제를 해결할 수 있습니다:
foo =+document.getElementById("foo-input").value;
^1진법의 단항연산자 +는 오른쪽에 있는 부분을 숫자로 바꿉니다.
// typeof foo;// "number"if ( foo ===1 ) {
importantTask();
}
// `importantTask()` 가 호출됩니다.
// 3.B.2.3var array = [ "a", "b", "c" ];
!!~array.indexOf( "a" );
// true!!~array.indexOf( "b" );
// true!!~array.indexOf( "c" );
// true!!~array.indexOf( "d" );
// false// 주의: 위의 구문들은 "과도하게 영리"한 방법이 아닌지 생각해 보아야 합니다// 다음 구문처럼 비교되는 indexOf의 리턴 값이 더 명확한 쪽이// 좋지 않을까요?if ( array.indexOf( "a" ) >=0 ) {
// ...
}
// 3.B.2.4var num =2.5;
parseInt( num, 10 );
// 위처럼 쓴 것은, 아래에 쓴 것과 같은 의미입니다.~~num;
num >>0;
num >>>0;
// 전부 2가 됩니다.// 하지만 음수는 다르게 취급되는것도 명심하셔야 합니다...var neg =-2.5;
parseInt( neg, 10 );
// 위처럼 쓴 것은, 아래에 쓴 것과 같은 의미입니다.~~neg;
neg >>0;
// 전부 -2가 됩니다.// 하지만...
neg >>>0;
// 이경우엔 4294967294가 나옵니다.
// 4.1.1// 배열에 뭔가가 들어있는지 여부를 확인하려는 거라면,// 다음과 같이 코드를 작성하는 대신에:if ( array.length>0 ) ...// 다음과 같이 작성하세요:if ( array.length ) ...// 4.1.2// 배열이 비어있다는 것만을 확인할 때에는,// 아래처럼 작성하지 마시고:if ( array.length===0 ) ...// ...다음처럼 작성하세요:if ( !array.length ) ...// 4.1.3// 문자열이 비어있지 않다는 것을 확인할 때에는,// 다음처럼 작성하지 마시고:if ( string !=="" ) ...// ...다음과 같이 작성하세요:if ( string ) ...// 4.1.4// 문자열이 _비어있다는 것_을 확인만 하는 경우라면,// 다음처럼 작성하지 마시고:if ( string ==="" ) ...// ...다음과 같이 작성해서, 거짓인지를 확인하세요. :if ( !string ) ...// 4.1.5// 참조 변수가 true인지 확인하려면,// 다음처럼 작성하지 마시고:if ( foo ===true ) ...// ...그냥 아래처럼 써주세요. 기본 기능을 활용하면 됩니다:if ( foo ) ...// 4.1.6// 어떤 참조 변수가 false인지 판정할 때에는,// 다음처럼 작성하지 마시고:if ( foo ===false ) ...// ...true인지를 확인하도록 부정(!)을 사용하세요.if ( !foo ) ...// ...주의하세요. 이렇게 제안하면 foo의 값이 0, “”, null, undefined, NaN인 경우에도 참을 반환할 겁니다.// foo가 불린값 false를 갖는지를 확인하는 경우라면, 아래와 같이 사용하세요.if ( foo ===false ) ...// 4.1.7// 어떤 변수가 있다고 하죠. 이 변수의 값은 null이나 undefined일 수는 있지만 false나 "", 또는 0의 값은 가지지 않습니다. 이런 변수를 판정할 때에는,// 아래처럼 작성하지 마시고:if ( foo ===null|| foo ===undefined ) ...// ...강제형변환되는 ==를 사용하세요. 다음과 같이요:if ( foo ==null ) ...// 그리고 이 점을 기억하세요. == 를 사용하면, 판정하려는 변수의 값이 `null` 이나 `undefined` 일 때, 참을 반환할 것입니다.// 하지만 `false` 나 "" 나 0 값을 가질 때에는 거짓을 반환할 것입니다.null==undefined
항상최선의 결과를 내는 비교가 무엇인지 고민하세요. - 위에 것들은 다 가이드라인입니다, 교리같은건 아니죠.
// 4.2.1// 형변환 과 비교에 관한 메모`===` 가 `==` 보다 좋습니다. (느슨한 형 비교가 필요하지 않는 이상)
=== 는 형변환을 하지 않습니다. 무슨뜻인가 하면:"1"===1;
// false== 는 형변환을 합니다, 무슨 뜻인가 하면:"1"==1;
// true// 4.2.2// 불린값, 참(true)으로 간주되는 것, 거짓(false)으로 간주되는 것
불린값:true, false
참(true)으로 간주되는 것:"foo", 1
거짓(false)으로 간주되는 것:"", 0, null, undefined, NaN, void0
// 5.1.1// 실용적인 모듈
(function( global ) {
var Module = (function() {
var data ="secret";
return {
// 이것은 불린 속성입니다
bool:true,
// 문자열 값
string:"a string",
// 배열 속성
array: [ 1, 2, 3, 4 ],
// 객체 속성
object: {
lang:"en-Us"
},
getData:function() {
// `data` 의 현재 값을 알아내고 반환합니다return data;
},
setData:function( value ) {
// `data` 의 값을 설정하고 반환합니다return ( data = value );
}
};
})();
// 여기에 다른 내용이 들어갈 수도 있습니다// 전역 객체에 모듈을 노출시키세요.global.Module= Module;
})( this );
// 5.2.1// 실용적인 생성자
(function( global ) {
functionCtor( foo ) {
this.foo= foo;
returnthis;
}
Ctor.prototype.getFoo=function() {
returnthis.foo;
};
Ctor.prototype.setFoo=function( val ) {
return ( this.foo= val );
};
// `new`없이 생성자를 호출하려면, 이렇게 할 수 도 있습니다:varctor=function( foo ) {
returnnewCtor( foo );
};
// 전역 개체에 생성자를 노출합니다global.ctor= ctor;
})( this );
A. 여러분은 컴파일러나 압축기(compressor)가 아닙니다. 그렇게 되려고 시도하지 마세요.
다음은 이름을 심하게 안 좋게 지은 코드 작성 사례입니다:
// 6.A.1.1// 이름을 안 좋게 지은 코드 작성 사례functionq(s) {
returndocument.querySelectorAll(s);
}
var i,a=[],els=q("#foo");
for(i=0;i<els.length;i++){a.push(els[i]);}
틀림없이 여러분은 이제까지 이렇게 코드를 작성했겠죠. - 이런 코드 작성 방식은 오늘부로 바뀌기를 바랍니다.
아래에는 좀 더 사려깊게, 그리고 좀 더 논리적으로, 친절하게 이름을 짓는 방식이 제시되어 있습니다. (가독성이 더욱 높은 구조이고요):
// 6.A.2.1// 이름짓기가 더 잘된 코드 작성 예functionquery( selector ) {
returndocument.querySelectorAll( selector );
}
var idx =0,
elements = [],
matches =query("#foo"),
length =matches.length;
for( ; idx < length; idx++ ){
elements.push( matches[ idx ] );
}
몇 가지 이름짓는 방법에 대한 조언 몇 가지:
// 6.A.3.1// strings의 이름 짓기`dog` 은 문자열입니다.
// 6.A.3.2// 배열의 이름 짓기`dogs` 는 `dog` 문자열이 들어있는 배열입니다.
// 6.A.3.3// 함수나 오브젝트(object), 객체(instance) 등의 이름 짓기
camelCase; 함수와 변수를 선언할 때는 이렇게 이름지어 주세요.
// 6.A.3.4// 생성자, prototypes, 기타 등등의 이름 짓는 법
PascalCase; 생성자 함수의 이름은 이렇게 짓습니다.
// 6.A.3.5// 정규표현식의 이름 짓는 법
rDesc =//;// 6.A.3.6// 구글 Closure 라이브러리 스타일 가이드에서 발췌한 것
functionNamesLikeThis; // 함수는 이렇게 이름을 지으시고..
variableNamesLikeThis; // 변수는 이렇게 이름을 지으세요.
ConstructorNamesLikeThis; // 생성자는 이렇게 이름을 지으시고..
EnumNamesLikeThis; // Enum의 이름은 이렇게 지으세요.
methodNamesLikeThis; // 메서드의 이름은 이렇게 지으시고,SYMBOLIC_CONSTANTS_LIKE_THIS; // symbolic 상수의 이름은 이렇게 지어주세요.
B.this다루기
나중에 호출되는BoundFunction을 정의 하기위해서call이나apply가 일반 적으로 쓰여지고 있긴 하지만,.bind( this )같은 함수를 사용하는편이 좋습니다. 더 좋은 선택지가 없을 때에만 엘리어싱을 고려하세요.
// 6.B.1functionDevice( opts ) {
this.value=null;
// 비동기 스트림으로 열어서,// 지속적으로 불릴 것 입니다.stream.read( opts.path, function( data ) {
// 이 인스턴스의 현재 값을// 데이터 스트림의 최신값으로 업데이트this.value= data;
}.bind(this) );
// 이 Device 인스턴스에서의 이벤트 발생 빈도를 조절setInterval(function() {
// 조절된 이벤트를 발생this.emit("event");
}.bind(this), opts.freq||100 );
}
// EventEmitter를 구현하는 척만 했어요. ;)
이런게 불가능할 경우를 위해, 많은 현대적인 자바스크립트 라이브러리들이.bind같은 함수를 가지고 있습니다.