netstat
: 실행중인 port 찾기
netstat -a -o
: 실행중인 port 표시, 프로세스id(pid) 표시
taskkill /f /pid 1234
: 1234 프로세스id(pid) kill하기
netstat
: 실행중인 port 찾기
netstat -a -o
: 실행중인 port 표시, 프로세스id(pid) 표시
taskkill /f /pid 1234
: 1234 프로세스id(pid) kill하기
리액트애서 애플리케이션을 만들 때, 기본적으로는 보통 하나의 루트 컴포넌트 (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 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 되죠.
리듀서 함수는 두가지의 파라미터를 받습니다.
그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.
상태가 변화가 생기면, 구독하고 있던 컴포넌트에게 알림
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 되죠.
정리
정리하자면 이렇습니다. 기존에는 부모에서 자식의 자식의 자식까지 상태가 흘렀었는데, 리덕스를 사용하면 스토어를 사용하여 상태를 컴포넌트 구조의 바깥에 두고, 스토어를 중간자로 두고 상태를 업데이트 하거나, 새로운 상태를 전달받습니다. 따라서, 여러 컴포넌트를 거쳐서 받아올 필요 없이 아무리 깊숙한 컴포넌트에 있다 하더라도 직속 부모에게서 받아오는 것 처럼 원하는 상태값을 골라서 props 를 편리하게 받아올 수 있죠.
리덕스는 리액트에 종속되는 그런 라이브러리가 아닙니다. 물론 리액트에서 쓰기위해 만든거니 궁합은 매우 잘맞죠! 한번, 평범한 HTML 와 JavaScript 환경에서 리덕스를 사용해가면서, 리덕스의 기본 개념을 배워봅시다.
우선, JSBin(https://jsbin.com/) 을 열으세요.
우리는 간단한 카운터를 구현해보겠습니다. HTML 섹션엔 다음과 같이 입력해보세요.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>그냥 평범한 리덕스</title> </head> <body> <h1 id="number">0</h1> <button id="increment">+</button> <button id="decrement">-</button> <script src="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'; // 액션 객체를 만들어주는 액션 생성 함수 const increment = (diff) => ({ type: INCREMENT, diff: diff }); const decrement = () => ({ type: DECREMENT }); // 초기값을 설정합니다. 상태의 형태는 개발자 마음대로 입니다. const initialState = { number: 0 }; /* 이것은 리듀서 함수입니다. state 와 action 을 파라미터로 받아옵니다. 그리고 그에 따라 다음 상태를 정의 한 다음에 반환해줍니다. */ // 여기에 state = initialState 는, 파라미터의 기본값을 지정해줍니다. const counter = (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 함수입니다 const render = () => { 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()); })
이제 버튼들을 눌러보시면 숫자가 변경 될 것입니다. 지금까지 한 작업들을 정리해봅시다.
리덕스 사용법 (0) | 2019.07.12 |
---|---|
javascript 네이밍 (0) | 2019.01.31 |
React 컴포넌트 작성법 (0) | 2019.01.29 |
자바스크립트 타이머 (0) | 2019.01.29 |
리액트 문법 (0) | 2019.01.28 |
리덕스를 프로젝트에서 사용하게 될 때 알아둬야 할 3가지 규칙이 있습니다.
하나의 애플리케이션에선 단 한개의 스토어를 만들어서 사용합니다. 사실, 권장되지는 않습니다. 여러개의 스토어를 만들고 싶다면 만들 수는 있습니다. 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 그렇게 여러개의 스토어를 만들 수도 있습니다. 하지만 그렇게 하면, 개발 도구를 활용하지 못하게 됩니다.
리액트에서 state 를 업데이트 해야 할 때, setState 를 사용하고, 배열을 업데이트 해야 할 때는 배열 자체에 push 를 직접 하지 않고, concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트를 합니다. 엄청 깊은 구조로 되어있는 객체를 업데이트를 할 때도 마찬가지로, 기존의 객체는 건들이지 않고 Object.assign 을 사용하거나 spread 연산자 (...) 를 사용하여 업데이트 하곤 하죠.
리덕스에서도 마찬가지입니다. 기존의 상태는 건들이지 않고 새로운 상태를 생성하여 업데이트 해주는 방식으로 해주면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있습니다.
리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여 shallow equality 검사를 하기 때문입니다. 이를 통하여 객체의 변화를 감지 할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있는 것이죠.
우리는 이 튜토리얼에서 Immutable.js 를 사용하여 불변성을 유지하며 상태를 관리하게 됩니다. 불변성과 Immutable.js 가 익숙하지 않다면 리액트의 불변함, 그리고 컴포넌트에서 Immutable.js 사용하기 포스트를 읽으시면 도움이 될거에요.
순수한 함수, 라는 개념이 익숙하지 않으시죠. 다음 사항을 기억해주세요.
3가지 사항을 주의해주세요. 동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 합니다. 그런데 일부 로직들 중에서는 실행 할 때마다 다른 결과값이 나타날 수도 있죠. new Date() 를 사용한다던지… 랜덤 숫자를 생성한다던지… 혹은, 네트워크에 요청을 한다던지! 그러한 작업은 결코 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해줘야 합니다. 그런것을 하기 위해서, 리덕스 미들웨어 를 사용하곤 하죠.
이번엔 리액트에서 리덕스를 사용하는 방법에 대해서 배워보겠습니다. 이 튜토리얼에서는, 제목에서 나타나 있듯이 ‘리덕스를 편하게 사용하기 위한 발악’ 을 해볼거에요. 리덕스 매뉴얼에서도 나타나는 정석대로만 한다면, 액션을 위한 파일과 리듀서를 위한 파일이 따로따로 구분되어있습니다. 정석대로만 하는 방법을 배우고 싶으시다면 카운터 만들기 튜토리얼을 읽어보세요.
우리는 리덕스 정석에서 좀 벗어나서, 편하게 사용 할 수 있는 방식으로 바로 학습하겠습니다. 추가적으로, 우리는 상태 관리를 할 때 불변성 (Immutability) 를 유지하기 위해서 Immutable.js 를 사용하겠습니다. 만약에 Immutable.js 의 사용법을 모른다면 React ❤️ Immutable.js – 리액트의 불변함, 그리고 컴포넌트에서 Immutable.js 사용하기 포스트를 한번 쭉 읽고 와주세요.
이번 실습을 진행하기 위해서, 리덕스 템플릿 프로젝트를 클론하세요.
$ git clone https://github.com/vlpt-playground/begin-redux.git
만약에 현재 직접 프로젝트를 클론 할 수 없는 상태라면 이 링크 를 눌러 직접 프로젝트를 살펴보세요.
그리고, 프로젝트의 디렉토리에서 필요한 패키지를 설치하세요.
$ yarn # 혹은 npm install
그 다음에 yarn start 를 하시면 다음과 같은 화면이 나타납니다.
우리가 앞으로 만들 프로젝트에는 두가지 프로그램이 구현되어있습니다. 하나는 숫자를 올리고 내리는 카운터이며, 두번째는 간단한 투두리스트입니다.
이 프로젝트는 현재 create-react-app 으로 만들어진 프로젝트에 다음 작업이 이뤄진 상태입니다.
0. 절대경로에서 파일을 불러 올 수 있도록 설정
우리가 파일들을 불러올때 import something from '../../../foo/something 이 아닌, src 디렉토리를 기준으로 절대경로를 입력하여 import something from 'foo/something 의 형태로 불러 올 수 있게 해줍니다.
1. 패키지 설치
리덕스를 사용하기 위하여 다음 패키지들이 설치되어있습니다.
2. 불필요한 파일 제거
다음 파일들이 제거되었습니다.
3. 주요 컴포넌트 생성 및 루트 컴포넌트 생성
프로젝트에서 필요한 컴포넌트들이 만들어졌습니다.
AppTemplate 의 경우 두가지 화면을 화면에 레이아웃 해주는 역할을 하고, Counter 와 Todos 는 카운터와 투두리스트의 뷰 만을 보여주는 컴포넌트입니다.
Root 은 우리 프로젝트의 최상위 컴포넌트 입니다.
CounterContainer 와 TodosContainer.js 는 현재 비어있는 컴포넌트들인데 이 컴포넌트들은 나중에 우리가 리덕스와 연동을 해 줄 컴포넌트입니다.
리덕스와 연동된 컴포넌트를 우리는 컨테이너 컴포넌트 라고 부릅니다. 반면, 단순히 뷰 만을 보여주기 위하여 만들어진 컴포넌트는 프리젠테이셔널 컴포넌트 라고 부릅니다. 리덕스를 사용하여 만든 리액트 애플리케이션에서는 대부분 이렇게 컴포넌트를 구분합니다. 이러한 패턴은 무조건 따라야 하는 것은 아니지만, 이렇게 하면 앞으로 프로젝트를 개발 할 때 매우 편해집니다.
4. 리덕스 관련 코드를 작성 할 파일 생성
다음 파일들은 리덕스 관련 코드를 작성하기 위하여 필요한 파일들입니다.
이 파일들은 현재 다 비어있으며 우리가 앞으로 채워나갈것입니다.
우리는 액션과 리듀서를 기능별로 분류하여 하나의 파일에 작성하게 되는데 이를 module 이라고 부릅니다. 예를들어 카운터에 관련된 코드는 counter.js 에서 작성하고, 투두리스트에 관련된건 todo.js 에 작성하게 되죠.
그리고, configure.js 는 리덕스 스토어를 생성하는 함수를 모듈화하여 내보냅니다. 이렇게 따로 모듈화를 하는 이유는, 하나의 애플리케이션에서는 하나의 스토어밖에 없긴 하지만 예외의 케이스가 있기 때문입니다. 나중에 여러분이 서버사이드 렌더링을 하게 된다면, 서버쪽에서도 각 요청이 처리 될 때마다 스토어를 생성해주어야 하는데요, 그런 작업을 하게 될 때 이렇게 스토어를 생성하는 함수를 이렇게 모듈화 하곤 합니다.
그리고, store/index.js 에선 스토어를 생성한다음에 내보내줍니다. 이렇게 모듈화된 스토어는 브라우저쪽에서만 사용되는 스토어입니다 (서버사이드 렌더링을 하게 될 땐 아까 언급했던 configure 를 통하여 그때 그때 만듭니다). 이렇게 모듈화된 스토어는 리액트 애플리케이션을 초기설정 할 때 사용됩니다.
actionCreators.js 에서도 스토어를 불러오고, 또 각 모듈들에서 선언 했던 액션 생성함수들을 불러와서 store 의 dispatch 와 미리 바인딩 작업을 해줍니다. (이 부분은 나중에 다루겠습니다.)
프로젝트의 각 파일들을 열어가면서 각 파일들이 어떠한 역할을 하는지 한번 살펴보세요.
카운터의 상태를 리덕스를 사용하여 관리해보겠습니다. 구현하기에 앞서, Counter.js 컴포넌트를 살펴봅시다.
import React from 'react';
const Counter = ({
number,
onIncrement,
onDecrement
}) => {
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrement}>증가 (+)</button>
<button onClick={onDecrement}>감소 (-)</button>
</div>
);
};
Counter.defaultProps = {
number: 0
}
export default Counter;
이 컴포넌트에서는, 숫자값 number 와, 값을 증가시키는 함수 onIncrement, 그리고 값을 감소시키는 함수 onDecrement 를 props 로 받아옵니다.
그럼, 여기서 필요한 리덕스 모듈을 작성해봅시다.
src/store/modules/counter.js// 액션 타입을 정의해줍니다. const INCREMENT = 'counter/INCREMENT'; const DECREMENT = 'counter/DECREMENT'; // 액션 생성 함수를 만듭니다. // 이 함수들은 나중에 다른 파일에서 불러와야 하므로 내보내줍니다. export const increment = () => ({ type: INCREMENT }); export const decrement = () => ({ type: DECREMENT }); // 모듈의 초기 상태를 정의합니다. const initialState = { number: 0 }; // 리듀서를 만들어서 내보내줍니다. export default function reducer(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 이라는 함수를 사용하면 액션 생성 함수 코드를 다음과 같이 작성 할 수 있게 됩니다.
src/store/modules/counter.jsimport { createAction } from 'redux-actions'; // 액션 타입을 정의해줍니다. const INCREMENT = 'counter/INCREMENT'; const DECREMENT = 'counter/DECREMENT'; // 액션 생성 함수를 만듭니다. export const increment = createAction(INCREMENT); export const decrement = createAction(DECREMENT); (...)
액션 생성함수에서 파라미터를 필요하게 되는 경우에도 createAction 을 사용 할 수 있습니다. 그 부분은 추후 우리가 todo 모듈을 작성하게 될 때 알아보겠습니다.
우리가 기존에 작성한 리듀서에서는 각 액션타입에 따라 다른 작업을 하기 위해서 switch 구문을 사용했었죠? switch 문은 block 으로 따로 나뉘어져 있는것이 아니기 때문에 이러한 작업은 못합니다:
switch(value) { case 0: const a = 1; break; case 1: 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'; // 액션 생성 함수를 만듭니다. export const increment = createAction(INCREMENT); export const decrement = createAction(DECREMENT); // 모듈의 초기 상태를 정의합니다. const initialState = { number: 0 }; // handleActions 의 첫번째 파라미터는 액션을 처리하는 함수들로 이뤄진 객체이고 // 두번째 파라미터는 초기 상태입니다. export default handleActions({ [INCREMENT]: (state, action) => { return { number: state.number + 1 }; }, // action 객체를 참조하지 않으니까 이렇게 생략을 할 수도 있겠죠? // state 부분에서 비구조화 할당도 해주어서 코드를 더욱 간소화시켰습니다. [DECREMENT]: ({ number }) => ({ number: number - 1 }) }, initialState);
지금은 리듀서가 하나밖에 없지만, 앞으로 우리가 todo 리듀서도 만들고 나면 한 프로젝트에 여러개의 리듀서가 존재하게 됩니다. 여러개의 리듀서가 있을 때에는, redux 의 함수 combineReducers 를 사용하여 하나의 리듀서로 합쳐줄 수 있습니다. 이렇게 합쳐진 리듀서는 루트 리듀서 라고 부릅니다.
src/modules/index.jsimport { combineReducers } from 'redux'; import counter from './counter'; export default combineReducers({ counter });
만약에 리듀서가 늘어나면 combineReducers({}) 부분에 더 추가를 해주면 됩니다.
스토어를 만드는 함수 configure 를 만들어서 내보내주겠습니다. 기본적으로는, 이렇게 작성하면 됩니다:
src/store/configure.jsimport { createStore } from 'redux'; import modules from './modules'; const configure = () => { const store = createStore(modules); return store; } export default configure;
우리는, 개발을 더 편하게 하기 위해서 redux-devtools 라는 크롬 익스텐션을 사용해볼건데요, 이를 사용하기 위해선 크롬 웹스토어 에서 설치를 하고, 스토어 생성 함수를 조금 바꿔주어야 합니다.
src/store/configure.jsimport { createStore } from 'redux'; import modules from './modules'; const configure = () => { // const store = createStore(modules); const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() const store = createStore(modules, devTools); return store; } export default configure;
방금 만든 configure 함수를 사용하여 스토어를 만들고, 내보내주겠습니다.
src/store/index.jsimport configure from './configure'; export default configure();
간단하지요?
리액트 앱에 리덕스를 적용 할 때에는, react-redux 에 들어있는 Provider 를 사용합니다. 프로젝트의 최상위 컴포넌트인 Root 컴포넌트를 열어서, Provider 와 우리가 방금 만든 store 를 불러온 뒤 다음과 같이 코드를 작성하세요.
src/Root.jsimport React from 'react'; import { Provider } from 'react-redux'; import store from './store'; import App from './components/App'; const Root = () => { return ( <Provider store={store}> <App /> </Provider> ); }; export default Root;
이제 리덕스와 연동된 컴포넌트인 CounterContainer 컴포넌트를 만들겠습니다. 일단, 컴포넌트를 만들어서 단순히 Counter 를 불러온다음에 렌더링하세요.
src/containers/CounterContainer.jsimport React, { Component } from 'react'; import Counter from 'components/Counter'; class CounterContainer extends Component { render() { return ( <Counter /> ); } } export default CounterContainer;
그리고 이 컴포넌트를 App 에서 불러와서 기존의 Counter 를 대체하겠습니다.
src/components/App.jsimport React, { Component } from 'react'; import CounterContainer from 'containers/CounterContainer'; import AppTemplate from './AppTemplate'; import Todos from './Todos'; class App extends Component { render() { return ( <AppTemplate counter={<CounterContainer />} todos={<Todos />} /> ); } } export default App;
컴포넌트가 그대로 렌더링되고 있나요? 그러면 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'; class CounterContainer extends Component { handleIncrement = () => { this.props.increment(); } handleDecrement = () => { this.props.decrement(); } render() { const { handleIncrement, handleDecrement } = this; const { number } = this.props; return ( <Counter onIncrement={handleIncrement} onDecrement={handleDecrement} number={number} /> ); } } // props 값으로 넣어 줄 상태를 정의해줍니다. const mapStateToProps = (state) => ({ number: state.counter.number }); // props 값으로 넣어 줄 액션 함수들을 정의해줍니다 const mapDispatchToProps = (dispatch) => ({ increment: () => dispatch(counterActions.increment()), decrement: () => dispatch(counterActions.decrement()) }) // 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다. // connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다. // 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다. export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
mapStateToProps 스토어의 상태를 파라미터로 받아오는 함수로서, 컴포넌트에 상태로 넣어줄 props 를 반환합니다.
mapDispatchToProps 는 dispatch 를 파라미터로 받아오는 함수로서, 컴포넌트에 넣어줄 액션 함수들을 반환합니다.
코드를 저장하고 카운터의 증가버튼와 감소버튼을 눌러보세요. 숫자가 바뀌나요?
보통은 위와 같은 코드처럼, mapStateToProps 와 mapDispatchToProps 를 따로 만들곤 하는데, 사람마다 차이가 있을 수 있겠지만 그냥 함수를 connect 내부에서 정의하면 코드가 조금 더 깔끔해집니다.
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'; class CounterContainer extends Component { (...) } export default connect( (state) => ({ number: state.counter.number }), (dispatch) => ({ increment: () => dispatch(counterActions.increment()), decrement: () => dispatch(counterActions.decrement()) }) )(CounterContainer);
그리고 지금 dispatch 를 보면 각 액션 함수마다 일일히 dispatch(actionCreator()) 형식으로 작성해야 된다는점이 조금 귀찮습니다. 이 부분은, redux 의 bindActionCreator 함수를 사용하면 더 간소화 할 수 있습니다.
src/containers/CounterContainer.jsimport React, { Component } from 'react'; import Counter from 'components/Counter'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as counterActions from 'store/modules/counter'; class CounterContainer extends Component { (...) } export default connect( (state) => ({ number: state.counter.number }), (dispatch) => bindActionCreators(counterActions, dispatch) )(CounterContainer);
코드가 좀 간소화됐죠? 나중에 가면 여러분이 만들 컨테이너 컴포넌트에서 여러 모듈에서 액션 생성 함수를 참조해야 하게 되는 일도 있습니다. 그러한 경우엔 다음과 같이 bindActionCreators 의 결과물을 CounterActions 라는 props 로 넣어주면 됩니다. 그리고 물론, 이에 따라 메소드들도 조금 바꿔줘야겠죠?
src/containers/CounterContainer.jsimport React, { Component } from 'react'; import Counter from 'components/Counter'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as counterActions from 'store/modules/counter'; class CounterContainer extends Component { handleIncrement = () => { const { CounterActions } = this.props; CounterActions.increment(); } handleDecrement = () => { const { CounterActions } = this.props; CounterActions.decrement(); } render() { const { handleIncrement, handleDecrement } = this; const { number } = this.props; return ( <Counter onIncrement={handleIncrement} onDecrement={handleDecrement} number={number} /> ); } } export default connect( (state) => ({ number: state.counter.number }), (dispatch) => ({ CounterActions: bindActionCreators(counterActions, dispatch) }) )(CounterContainer);
이제 카운터 코드는 모두 작성하였습니다. 코드를 저장하고 카운터가 제대로 작동하는지 확인해보세요.
자 이제 우측에 보여지는 투두리스트를 구현해보겠습니다. 투두리스트는 카운터와 달리 필요한 액션의 수가 좀 더 많고, 상태의 구조도 아주 조금 더 복잡합니다.
우선, 필요한 액션들은 다음과 같습니다:
그리고 상태의 구조는 다음과 같습니다:
{ input: '', todos: [ { id: 0, text: '걷기', checked: false }, { id: 1, text: '코딩하기', checked: true } ] }
우리는 상태를 업데이트 할 때, 기존의 객체는 건들이지 않고 새 객체를 만들어주어야 합니다. 즉, 불변성을 유지해가면서 상태를 업데이트해야한다는 것이죠.
다음 예제를 한번 읽어보세요.
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 모듈을 작성해봅시다! 먼저 액션 생성함수들을 작성해볼까요?
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'; export const changeInput = createAction(CHANGE_INPUT); export const insert = createAction(INSERT); export const toggle = createAction(TOGGLE); export const remove = createAction(REMOVE);
우리가 이번에 만든 액션함수들은, 참조해야 할 값들이 필요합니다. 예를들어서, changeInput 은 다음 어떤 값으로 바뀌어야 할지를 알려주는 값이 필요하고, insert 는 추가 할 내용, 그리고, toggle 과 remove 는 어떤 id 를 수정해야 할 지 알려주어야겠죠.
createAction 을 통하여 만든 액션생성함수에 파라미터를 넣어서 호출하면, 자동으로 payload 라는 이름으로 통일되어 설정됩니다.
다음과 같이 말이죠:
changeInput('새로운 값'); // { type: 'todo/CHANGE_INPUT', 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'; export const changeInput = createAction(CHANGE_INPUT, value => value); export const insert = createAction(INSERT, text => text); export const toggle = createAction(TOGGLE, id => id); export const 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'; export const changeInput = createAction(CHANGE_INPUT, value => value); export const insert = createAction(INSERT, text => text); export const toggle = createAction(TOGGLE, id => id); export const remove = createAction(REMOVE, id => id); let id = 0; // todo 아이템에 들어갈 고유 값 입니다 const initialState = Map({ input: '', todos: List() }); export default handleActions({ // 한줄짜리 코드로 반환 할 수 있는 경우엔 다음과 같이 블록 { } 를 생략 할 수 있습니다. [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'; export default combineReducers({ counter, todo });
우리가 CounterContainer 에 했던것과 동일한 작업을 진행해주겠습니다. TodosContainer 에서 Todos 를 불러와서 렌더링 하고, 기존에 App 에서 Todos 가 들어가던 자리를 TodosContainer 로 대체해주는 것이죠.
src/containers/TodosContainer.jsimport React, { Component } from 'react'; import Todos from 'components/Todos'; class TodosContainer extends Component { render() { return ( <Todos /> ); } } export default TodosContainer; src/components/App.jsimport React, { Component } from 'react'; import CounterContainer from 'containers/CounterContainer'; import TodosContainer from 'containers/TodosContainer'; import AppTemplate from './AppTemplate'; class App extends Component { render() { return ( <AppTemplate counter={<CounterContainer />} todos={<TodosContainer />} /> ); } } export default App;
본격적으로 시작하기 전에, Todos 컴포넌트를 한번 살펴볼까요?
src/components/Todos.jsimport React from 'react'; import { List, Map } from 'immutable'; const TodoItem = ({ id, text, checked, onToggle, onRemove }) => ( <li style={{ textDecoration: checked ? 'line-through' : 'none' }} onClick={() => onToggle(id)} onDoubleClick={() => onRemove(id)}> {text} </li> ) const Todos = ({todos, input, onInsert, onToggle, onRemove, onChange }) => { const todoItems = todos.map( todo => { const { id, checked, text } = todo.toJS(); return ( <TodoItem id={id} checked={checked} text={text} onToggle={onToggle} onRemove={onRemove} key={id} /> ) } ) return ( <div> <h2>오늘 할 일</h2> <input value={input} onChange={onChange}/> <button onClick={onInsert}>추가</button> <ul> { todoItems } </ul> </div> ); }; Todos.defaultProps = { todos: List([ Map({ id: 0, text: '걷기', checked: false }), Map({ id: 1, text: '코딩하기', checked: true }) ]), input: '' }; export default Todos;
그냥 전형적인 투두리스트입니다. 할일목록이 들어있는 todos 값과, 인풋 내용 input 값을 받아옵니다. 그리고 4가지 함수도 props 로 받아오죠.
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'; class TodosContainer extends Component { 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 ( <Todos input={input} todos={todos} onChange={handleChange} onInsert={handleInsert} onToggle={handleToggle} onRemove={handleRemove} /> ); } } export default connect( // state 를 비구조화 할당 해주었습니다 ({ todo }) => ({ // immutable 을 사용하니, 값을 조회 할 때엔느 .get 을 사용해주어야하죠. input: todo.get('input'), todos: todo.get('todos') }), (dispatch) => ({ TodoActions: bindActionCreators(todoActions, dispatch) }) )(TodosContainer);
이제 투두리스트에서 인풋을 수정해보고, 버튼을 클릭해서 새 투두아이템을 생성해보세요. 그리고 생성된 아이템을 클릭하여 삭제선을 껐다 켜보시고, 더블클릭하여 제거해보세요.
축하합니다! 우리가 구현해야 할 주요 작업들은 모두 끝났습니다. 하지만, 이 튜토리얼은 완전히 끝나지는 않았습니다. 앞으로 해결해야 할 항목이 두가지 남았습니다. 근데 이것들은 꼭 해야 하는 것은 아닙니다. 다만, 개발을 어쩌면 조금 더 편하게 해줄 수 는 있습니다.
리덕스를 사용하면서 의문점이 들었습니다. 지금의 경우엔, CounterContainer 에서 counter 모듈의 액션생성함수를 참조하고, TodosContainer 에서 todo 모듈의 액션생성함수를 참조하고 있는데요, 실제 프로젝트에서는 한 종류의 모듈을 여러곳에서 사용 할 일이 많습니다. 예를들어서, form 이라는 모듈에서 폼 만을 관리하는 모듈을 만들 수도 있는 것이고, modal 이라는 모듈을 만들어서 모든 모달들을 관리 할 수도 있고… 또 header 라는 모듈을 만들어서 헤더에 관련된 액션들을 관리 하게 될 수도 있죠.
그런데, 그러한 액션들을 사용 할 때마다 mapDispatchToProps 에 해당하는 부분을 계속 작성하는 것이 저는 굉장히 귀찮다고 생각했습니다. 그래서, 최근 액션생성함수를 미리 bind 하는 것을 시도해봤는데, 꽤 만족스러웠어서 여기에도 소개 해볼까 합니다.
일단, 이것을 하기 위해선, 리덕스 스토어 인스턴스가 모듈화되어 불러 올 수 있는 상태여야 합니다. (우리는 이미 그렇게 했죠) 참고로 리덕스 매뉴얼에서 FAQ 란을 보면 스토어 인스턴스를 모듈화하여 내보내는것을 권장하고 있지 않다고 적혀있는데 그 이유는 나중에 리덕스 앱을 분리시킬때 힘들것이기 때문이라고 적어놓았는데요, 여러개의 컴포넌트에서 스토어를 직접 불러와서 접근하는 것이 아니기 때문에 이 부분은 문제되지 않습니다.
자! 그러면 액션생성함수를 미리 bind 해봅시다!
src/store/actionCreators.jsimport { bindActionCreators } from 'redux'; import * as counterActions from './modules/counter'; import * as todoActions from './modules/todo'; import store from './index'; const { dispatch } = store; export const CounterActions = bindActionCreators(counterActions, dispatch); export const TodoActions = bindActionCreators(todoActions, dispatch);
그러면, CounterContainer 는 다음과 같이 수정하여 사용 할 수 있습니다.
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'; class CounterContainer extends Component { handleIncrement = () => { CounterActions.increment(); } handleDecrement = () => { CounterActions.decrement(); } render() { const { handleIncrement, handleDecrement } = this; const { number } = this.props; return ( <Counter onIncrement={handleIncrement} onDecrement={handleDecrement} number={number} /> ); } } /* 첫번째 파라미터 mapStateToProps: props 값으로 넣어 줄 상태를 정의해줍니다. 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다. connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다. 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다. */ export default connect( (state) => ({ number: state.counter.number }) )(CounterContainer);
이렇게 mapDispatchToProps 는 생략 할 수 있게 되죠.
TodosContainer 도 마찬가지 입니다.
src/containers/TodosContainer.jsimport React, { Component } from 'react'; import Todos from 'components/Todos'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { TodoActions } from 'store/actionCreators'; class TodosContainer extends Component { handleChange = (e) => { TodoActions.changeInput(e.target.value); } handleInsert = () => { const { input } = this.props; TodoActions.insert(input); TodoActions.changeInput(''); } handleToggle = (id) => { TodoActions.toggle(id); } handleRemove = (id) => { TodoActions.remove(id); } render() { const { handleChange, handleInsert, handleToggle, handleRemove } = this; const { input, todos } = this.props; return ( <Todos input={input} todos={todos} onChange={handleChange} onInsert={handleInsert} onToggle={handleToggle} onRemove={handleRemove} /> ); } } export default connect( ({ todo }) => ({ input: todo.input, todos: todo.todos }) )(TodosContainer);
아직 이 방법은 실험적입니다. 무조건 이렇게 하라고 권장하지는 않겠습니다 🙂 하지만, 여러분들이 만약에 앞으로 작업을 하면서 mapDispatchToProps 를 일일히 하는것이 귀찮아진다고 느낄때면, 이러한 방법이 있다는 것을 참고하세요~
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'; export const changeInput = createAction(CHANGE_INPUT, value => value); export const insert = createAction(INSERT, text => text); export const toggle = createAction(TOGGLE, id => id); export const 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 }) export default handleActions({ [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'; class TodosContainer extends Component { (...) } export default connect( ({ 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 ( <TodoItem id={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 를 사용하는 것이 훨씬 쉬운것은 사실입니다. 하지만 나중에 상태와 업데이트 방식의 종류가 많아진다면, 리덕스가 없으면 정말 관리하기 복잡해집니다.
리덕스 왜쓸까 (0) | 2019.07.12 |
---|---|
javascript 네이밍 (0) | 2019.01.31 |
React 컴포넌트 작성법 (0) | 2019.01.29 |
자바스크립트 타이머 (0) | 2019.01.29 |
리액트 문법 (0) | 2019.01.28 |
타임리프 문법 및 표현방법 정리
th:text
th:text는 태그 안에 들어가는 텍스트 값이다.
1 |
<span th:text="${eventFvrDtl.winRnkg}"></span> |
cs |
th:if, th:unless, th:value
th:if는 if, th:unless는 else 표현이다.
th:value는 태그 안의 value이다.
1 2 3 4 5 6 |
<th:block th:if="${eventPtcpPsbOrdData != null && #lists.size(eventPtcpPsbOrdData.eventPtcpOrdInfoList) > 0}"> <input type="hidden" id="ibx_TotalPurAplAmt" th:value="${totalPurAplAmt}"/> </th:block> <th:block th:unless="${eventPtcpPsbOrdData != null && #lists.size(eventPtcpPsbOrdData.eventPtcpOrdInfoList) > 0}"> <input type="hidden" id="ibx_TotalPurAplAmt" value="0"/> </th:block> |
cs |
th:utext (unescaped text)
th:utext는 <div></div>같은 태그형식의 코드를 삽입하고 싶을때 사용한다.
태그형식의 텍스트 들어올시 태그로 인식함.
1 2 |
<!-- HTML 컨텐츠 영역 --> <th:block th:utext="${#campaignUtils.unescapeHtml(eventDispTempleteInfo.htmlContents)}"></th:block> |
cs |
th:with
th:with는 변수형태의 값을 재정의한 것이다.
stnmZipNo의 변수를 값이 있다면 정의하고 없다면 공백으로 정의한다.
1 2 |
<th:block th:with="stnmZipNo=${#strings.defaultString(ecCustInfo.stnmZipNo, '')}"> </th:block> |
cs |
th:switch , th:case
switch case 문과 같다.
fvrDvsCd값이 이럴때 case1, case2 빠지고
그 외에 것은 th:case=* 로 빠지게 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<div th:switch="${eventFvrDtl.fvrDvsCd}" class="coupon_gift"> <p th:case="${#foConstants.FVR_DVS_CD_DISCOUNT}" class="cp cptype02"> <th:block th:switch="${fvrKnd2Cd}"> <span th:case="${#foConstants.FVR_KND2_CD_FREE_DLV_CPN}" th:text="${fvrKnd2CdNm}" class="tx"></span> <span th:case="*" class="tx"> <th:block th:text="${fvrKnd2CdNm}"></th:block> </span> </th:block> </p> <p th:case="${#foConstants.FVR_DVS_CD_ACCUMULATION}" class="cp cptype02"> <span class="ty" th:text="${fvrKnd2CdNm}"></span> <span class="tx" th:text="${#numbers.formatInteger(eventFvrDtl.fvrDtlCnts, 3, 'COMMA')}"></span> </p> <p th:case="*" class="cp cptype02"> <span class="tx" th:text="${eventFvrDtl.fvrDtlCnts}"></span> </p> </div> |
cs |
th:fagment
include와 비슷하다.
특정 영역을 가져와서 나타낼 수 있다.
예를 들어 페이지마다 각각의 게시판을 가지고 싶은 경우
포함하는 곳.
ex) eventPage1.html
1 2 3 4 |
<th:block th:if="${#lists.size(eventDispTemplateCornerList)} > 0" th:each="eventDispTemplateCorner : ${eventDispTemplateCornerList}"> <!-- 이벤트 템플릿 코너 --> <th:block th:include="${eventDispTemplateCorner.cmpnTmplFileUrl} :: corner (${eventDispTemplateCorner.cmpnNo}, ${eventData.eventBaseInfo.eventPtcpPsbYn})"></th:block> </th:block> |
cs |
받는 곳.
ex) board.html
1 2 3 4 5 |
<th:block th:fragment="corner (cmpnNo, eventPtcpPsbYn)"> <div class="content_title noline"> <p class="title">게시판</p> </div> </th:block> |
cs |
controller를 거쳐 화면으로 가져온 데이터를 스크립트로 제어할때
1 2 3 4 |
// controller ModelAndView mv; mv.addObject("eventData", eventData); return mv |
cs |
1 2 3 |
// controller를 거쳐 화면으로 가져온 데이터를 스크립트로 제어할때 var data1 = [[${eventData}]]; var eventPtcpPsbYn = [[${#strings.defaultString(eventData.eventBaseInfo.eventPtcpPsbYn, 'Y')}]]; |
cs |
태그 안의 attribute를 타임리프로 정의할때
1 2 3 4 5 6 7 |
// 태그 안의 attribute를 타임리프로 정의할때 <div id="myDiv1" th:attr="usemap=|#E_${eventData.eventBaseInfo.cmpnNo}|"> </div>
// 정의된 결과 <div id="myDiv1" usemap="#E_21082"> </div> |
cs |
HTTP 에러 (0) | 2016.08.29 |
---|---|
JAR 파일에 JSP 파일 넣는 방법 (0) | 2016.08.23 |
Html, Mybitis, Ibitis 특수문자 태크정리 (0) | 2016.06.22 |
ext js 환경 (0) | 2016.06.03 |
CSS 란 (0) | 2014.11.18 |
"스타일에 관한 논쟁은 무의미하다. 스타일 가이드가 있을테니, 거기에 따르기만 해라"
Rebecca Murphey
"성공적인 프로젝트의 멋진 일원이 되기 위해서는 여러분 마음대로 코드를 작성하는 것이 나쁜 생각™임을 깨닫는 것이지요. 수천만 사람들이 여러분의 코드를 사용한다면, 가장 명확하게 코딩해야 합니다. 언어 명세가 허용하는 한 가장 똑똑한 코드를 만들겠다는 당신의 개인적인 선호에 따라서가 아니라"
Idan Gazit
아래 리스트는 1) 이 주제의 모든 지식들을 포괄하지는 않습니다. 2) 반드시 읽기를 권합니다. 저는 아래 저자들이 말하는 코딩 스타일에 대해 항상 찬성하는 것은 아니지만, 분명한 것은 그들이 일관성 있단 겁니다. 게다가 이들은 프로그래밍 언어의 권위자들입니다.
프로젝트는 배포를 위해 소스코드를 압축하는 방법을 마련해두어야 합니다. 이렇게 하기 위해, Ben Alman의 grunt 는 둘째가라면 서러운 솔루션이며 공식적으로 이 레포지터리의 "kits/" 디랙토리를 대체 하고있습니다.
프로젝트에는 유닛, 참조, 구현, 기능에 관한 테스트가 들어가 있어야 합니다. 유스 케이스를 "테스트"로 한정하지 마세요. 아래에는 테스트를 하는 프레임워크의 목록을 나열해 두었습니다. 이 중에 어느 하나를 더욱 추천하는 것은 아닙니다.
이 글의 섹션은 현대적인 자바스크립트 개발을 위한 합리적인 스타일가이드를 제시하고 있지만, 그게 규정이라는 의미는 아닙니다. 제일 중요한 것은 일관성 있는 코딩 스타일을 위한 규칙 입니다. 프로젝트에 어떤 스타일을 사용하던간에 반드시 지켜져야 합니다. 이 문서를 프로젝트의 일관성, 가독성, 유지보수용이성을 위한 참고자료로써 링크해 주세요.
A. 중괄호{}, 괄호(), 줄 바꾸기
// 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도 같은 맥락으로 이 규칙이 적용됩니다.
// 나쁜 스타일
function foo() {
// 여기에 어떤 구문이 있음
var bar = "",
qux;
}
// 좋은 스타일
function foo() {
var bar = "",
qux;
// 변수 선언이후에 모든 구문들이 옴.
}
// 2.B.2.1
// 일반 함수 선언
function foo( arg1, argN ) {
}
// 사용법
foo( arg1, argN );
// 2.B.2.2
// 일반 함수 선언
function square( number ) {
return number * number;
}
// 사용법
square( 10 );
// 아주 부자연스러운 연속 전달 스타일(continuation passing style) (CPS에 대해서는 http://goo.gl/TA32o를 참고)
function square( number, callback ) {
callback( number * number );
}
square( 10, function( square ) {
// 콜백 문장
});
// 2.B.2.3
// 함수 표현식
var square = function( number ) {
// 가치 있고 의미 있는 뭔가를 반환합니다
return number * number;
};
// 식별자를 지닌 함수 표현식
// 아래와 같은 형태는 자기 자신을 호출할 수 있으면서
// 스택 트레이스상에서 식별할 수 있다는 부가적인 장점이 있습니다:
var factorial = function factorial( number ) {
if ( number < 2 ) {
return 1;
}
return number * factorial( number-1 );
};
// 2.B.2.4
// 생성자 선언
function FooBar( options ) {
this.options = options;
}
// 사용법
var fooBar = new FooBar({ 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.1
if (조건) {
// 코드
}
while (조건) {
// 코드
}
for (var i = 0; i < 100; i++) {
// 코드
}
if (true) {
// 코드
} else {
// 코드
}
E. 따옴표
작은 따옴표로 하냐 큰 따옴표로 하냐는 별로 중요하지않습니다, 자바스크립트에서는 그둘은 파싱하는 과정에서 전혀 차이가 없으니까요. 중요한것은 절대적인 일관성 이 있어야 한다는 점입니다. 같은 프로젝트에서는 절대로 큰따옴표와 작은따옴표를 섞지 마세요. 한 스타일을 정해서 고수하세요.
F. 각 줄의 끝부분, 빈 줄
여백이 있으면 바뀐 내용들을 읽을 수가 없게 됩니다. 또한 diff툴이 제대로 작동하지 않습니다. 줄의 끝에 있는 여백이나 빈 줄에 들어있는 공백을 자동적으로 제거해주는 ‘pre-commit hook'을 사용해 보세요.
자료형 확인하기 (Courtesy jQuery Core Style Guidelines)
A. 실제 타입
스트링:
typeof variable === "string"
숫자:
typeof variable === "number"
불린:
typeof variable === "boolean"
오브젝트:
typeof variable === "object"
배열:
Array.isArray(arrayObject)
(가능한 곳이면 어디든지)
노드:
elem.nodeType === 1
널(null):
variable === null
널(null), 미할당(undefined):
variable == null
미할당(undefined):
전역변수들:
typeof variable === "undefined"
지역변수들:
variable === undefined
프로퍼티:
object.prop === undefined
object.hasOwnProperty( prop )
"prop" in object
B. 강제변환된 타입
다음의 결과를 생각해 보죠...
아래와 같은 HTML 코드가 있어요:
<input type="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.1
var number = 1,
string = "1",
bool = false;
number;
// 1
number + "";
// "1"
string;
// "1"
+string;
// 1
+string++;
// 1
string;
// 2
bool;
// false
+bool;
// 0
bool + "";
// "false"
// 3.B.2.2
var number = 1,
string = "1",
bool = true;
string === number;
// false
string === number + "";
// true
+string === number;
// true
bool === number;
// false
+bool === number;
// true
bool === string;
// false
bool === !!string;
// true
// 3.B.2.3
var 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.4
var 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, void 0
// 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 ) {
function Ctor( foo ) {
this.foo = foo;
return this;
}
Ctor.prototype.getFoo = function() {
return this.foo;
};
Ctor.prototype.setFoo = function( val ) {
return ( this.foo = val );
};
// `new`없이 생성자를 호출하려면, 이렇게 할 수 도 있습니다:
var ctor = function( foo ) {
return new Ctor( foo );
};
// 전역 개체에 생성자를 노출합니다
global.ctor = ctor;
})( this );
A. 여러분은 컴파일러나 압축기(compressor)가 아닙니다. 그렇게 되려고 시도하지 마세요.
다음은 이름을 심하게 안 좋게 지은 코드 작성 사례입니다:
// 6.A.1.1
// 이름을 안 좋게 지은 코드 작성 사례
function q(s) {
return document.querySelectorAll(s);
}
var i,a=[],els=q("#foo");
for(i=0;i<els.length;i++){a.push(els[i]);}
틀림없이 여러분은 이제까지 이렇게 코드를 작성했겠죠. - 이런 코드 작성 방식은 오늘부로 바뀌기를 바랍니다.
아래에는 좀 더 사려깊게, 그리고 좀 더 논리적으로, 친절하게 이름을 짓는 방식이 제시되어 있습니다. (가독성이 더욱 높은 구조이고요):
// 6.A.2.1
// 이름짓기가 더 잘된 코드 작성 예
function query( selector ) {
return document.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.1
function Device( 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
같은 함수를 가지고 있습니다.
// 6.B.2
// lodash/underscore 의 _.bind()
function Device( opts ) {
this.value = null;
stream.read( opts.path, _.bind(function( data ) {
this.value = data;
}, this) );
setInterval(_.bind(function() {
this.emit("event");
}, this), opts.freq || 100 );
}
// jQuery.proxy
function Device( opts ) {
this.value = null;
stream.read( opts.path, jQuery.proxy(function( data ) {
this.value = data;
}, this) );
setInterval( jQuery.proxy(function() {
this.emit("event");
}, this), opts.freq || 100 );
}
// dojo.hitch
function Device( opts ) {
this.value = null;
stream.read( opts.path, dojo.hitch( this, function( data ) {
this.value = data;
}) );
setInterval( dojo.hitch( this, function() {
this.emit("event");
}), opts.freq || 100 );
}
최후의 수단으로, this
의 알리아스를 self
로 만드세요. 이 방법은 버그가 매우 발생하기 쉬우므로 가능한한 피해야 합니다.
// 6.B.3
function Device( opts ) {
var self = this;
this.value = null;
stream.read( opts.path, function( data ) {
self.value = data;
});
setInterval(function() {
self.emit("event");
}, opts.freq || 100 );
}
C. thisArg
사용하기
ES 5.1 의 내부 프로토타입의 함수는 thisArg
라는 특별한 시그니쳐를 가지고 있고 가능한 한 사용해야합니다
// 6.C.1
var obj;
obj = { f: "foo", b: "bar", q: "qux" };
Object.keys( obj ).forEach(function( key ) {
// |this| 는 이제 `obj`에 대한 참조
console.log( this[ key ] );
}, obj ); // <-- 이 최후의 인수가 `thisArg`
// Prints...
// "foo"
// "bar"
// "qux"
thisArg
는 Array.prototype.every
, Array.prototype.forEach
, Array.prototype.some
, Array.prototype.map
, Array.prototype.filter
같은 것과 같이 쓸 수 있습니다.
이번 섹션에서는 몇 가지 아이디어와 개념에 대해서 다룰 겁니다. 아이디어와 개념의 목적은 자바스크립트로 프로그래밍하는 데 있어 좀더 나은 방법을 찾을 수 있도록 기존에 사용하던 관행에 의문을 제기하게 만드는 것입니다.
A. switch
사용을 피해야 합니다. 현대의 방법론에서는, switch문이 들어간 함수를 블랙리스트에 올릴 겁니다.
파이어폭스와 크롬 최신 버전에서 switch
구문의 실행에 관한 눈부신 발전이 이루어진 듯 합니다.http://jsperf.com/switch-vs-object-literal-vs-module
개선된 것들 중에 주목할 만한 것은 이곳에서도 볼 수 있습니다: https://github.com/rwldrn/idiomatic.js/issues/13
// 7.A.1.1
// switch 구문을 사용한 코드 작성 예
switch( foo ) {
case "alpha":
alpha();
break;
case "beta":
beta();
break;
default:
// 기본적으로 수행할 것
break;
}
// 7.A.1.2
// 객체 리터럴(object lieteral)이나 모듈을 사용하여 접근하는 방식이 더 좋습니다:
var switchObj = {
alpha: function() {
// 구문들
// 반환문
},
beta: function() {
// 구문들
// 반환문
},
_default: function() {
// 구문들
// 반환문
}
};
var switchModule = (function () {
return {
alpha: function() {
// 구문들
// 반환문
},
beta: function() {
// 구문들
// 반환문
},
_default: function() {
// 구문들
// 반환문
}
};
})();
// 7.A.1.3
// `foo`가 `switchObj` 또는 `switchModule`의 속성이라고 할 때, 아래 메서드처럼 실행합니다...
( Object.hasOwnProperty.call( switchObj, foo ) && switchObj[ foo ] || switchObj._default )( args );
( Object.hasOwnProperty.call( switchObj, foo ) && switchModule[ foo ] || switchModule._default )( args );
// `foo` 의 값을 알고 있고 확신한다면
// 실행만 남겨 둔 채 OR 체크를 생략할 수도 있습니다:
switchObj[ foo ]( args );
switchModule[ foo ]( args );
// 이 패턴은 코드의 재사용성도 높여줘요.
B. 값을 일찍 반환하면 코드의 가독성이 높아져요. 성능 차이는 무시할 정도밖에 안돼죠.
// 7.B.1.1
// 안좋은 예:
function returnLate( foo ) {
var ret;
if ( foo ) {
ret = "foo";
} else {
ret = "quux";
}
return ret;
}
// 좋은 예:
function returnEarly( foo ) {
if ( foo ) {
return "foo";
}
return "quux";
}
기본적으로 중요한 것은 이겁니다:
이 개념에 대해 좀 더 알고 싶으시면, 다음 프리젠테이션을 보세요:
프로그램은 유지보수 하는 사람들에 의해 가급적 하나의 언어(영어이든 아니든 무슨 언어로든 간에 - 역자 주)로 쓰이는 것이 좋습니다.
이 문서를 기본 스타일 가이드로 인용하는 모든 프로젝트는 달리 명시하지 않는 한, 쉼표를 맨 앞에 쓰는 코드 포맷은 허락되지 않습니다.
리덕스 왜쓸까 (0) | 2019.07.12 |
---|---|
리덕스 사용법 (0) | 2019.07.12 |
React 컴포넌트 작성법 (0) | 2019.01.29 |
자바스크립트 타이머 (0) | 2019.01.29 |
리액트 문법 (0) | 2019.01.28 |
const FunctionalComponent = () => (
<div>
<h1>함수형 컴포넌트</h1>
<h2>Time : {new Date().toLocaleTimeString()}</h2>
</div>
);
class StatelessComponent extends React.Component {
render() {
return (
<div>
<h1>클래스형 컴포넌트, 상태가 없는 컴포넌트</h1>
<h2>Time : {new Date().toLocaleTimeString()}</h2>
</div>
);
}
}
class StatefulComponent extends React.Component {
//constructor 생성자
constructor(props) {
super(props);
this.state = {
now: new Date().toLocaleTimeString()
};
}
render() {
return (
<div>
<h1>클래스형 컴포넌트, 상태가 있는 컴포넌트</h1>
<h2>Time : {this.state.now}</h2>
</div>
);
}
}
ReactDOM.render(
<div>
<FunctionalComponent />
<StatelessComponent />
<StatefulComponent />
</div>
, document.getElementById('root'),
);
리덕스 왜쓸까 (0) | 2019.07.12 |
---|---|
리덕스 사용법 (0) | 2019.07.12 |
javascript 네이밍 (0) | 2019.01.31 |
자바스크립트 타이머 (0) | 2019.01.29 |
리액트 문법 (0) | 2019.01.28 |
setTimeOut (F, 시간)
- 시간이 지난후 동작
setInterval( F, 시간)
- 주기적으로 동작
clear 기능이 포함되어있음.
예제
function tick() {
const element = (
<div>
<h1>Hello, world</h1>
<h2>Time : {new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
const nTimer = setInterval(tick, 1000);
const nTimer1 = setTimeout(function(){
clearInterval(nTimer);
}, 5000);
console.log(nTimer, nTimer1);
리덕스 왜쓸까 (0) | 2019.07.12 |
---|---|
리덕스 사용법 (0) | 2019.07.12 |
javascript 네이밍 (0) | 2019.01.31 |
React 컴포넌트 작성법 (0) | 2019.01.29 |
리액트 문법 (0) | 2019.01.28 |
let a = 1;
const b = 2;
console.log(a);
function test(){
let a = 3;
let sum = 0;
for(let i=0; i<i; ++i){
let b = 3;
sum += i;
}
return sum;
}
let test1 = ()=>{
let sum = 0;
for(let i=0; i<i; ++i){
let b = 3;
sum += i;
}
return sum;
}
let test2 = ()=>{
let sum = 0;
for(let i=0; i<i; ++i){
let b = 3;
sum += i;
}
return `${sum}ddddd`;
}
const obj ={
x:1,
y:2,
u:3
}
let test3 = ()=>{
let sum = 0;
let {x,...testqq} = obj;
for(let i=0; i<i; ++i){
let b = 3;
sum += i;
}
return {
sum,i
}
}
ReactDOM.render(<App />, document.getElementById('root'));
필요 출처 사이트
https://babeljs.io/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=DYUwLgBAhhC8EEYDcAoAxgewHYGdICM4IAmVFdbHDUAOmAwHMAKKASjJQDMBXLNMAJbYIYEHiasA3igCQoSDHgBmVBAgo18iDm4BbIgAZVEThgBOTLQNhGIAgDwCkEANQuBUjRDngIhZcZqOvou8E5eAL5eZuDcZljaeqhRKFqieAhEErAAfNIQWsGGxqYWVjbODk6u7p5qPgREKl5Beq5hxlFqMWBxCcHJ5GliYMRZrLn5hW3wRl6llr7WtlXObh753lr-EM1qrSEdkdGx8RAABgAkksERACYPD-eDFLiQGPgAVnDSMgAeAC4EAAaLwATwBxFBam4AKUKBSwzwSnGkw0030s2MWkkf2BNAJ6TAAEdiREiB9PiVzItIMtKo41rVNg0_E1AolDnZOidemcWcFgQJjikAEogKD8AAiAHkALI0GJYO4gCz2ACCAAdNRAAPQ5YEQO4YNB6EBYMA0BjgACioF05rAACEwQBJO5MADkZgwGDAntY7HIwZDoZQQA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=es2015%2Creact%2Cstage-2&prettier=false&targets=&version=6.26.0&envVersion=
리덕스 왜쓸까 (0) | 2019.07.12 |
---|---|
리덕스 사용법 (0) | 2019.07.12 |
javascript 네이밍 (0) | 2019.01.31 |
React 컴포넌트 작성법 (0) | 2019.01.29 |
자바스크립트 타이머 (0) | 2019.01.29 |
- HTTP 에서 에러가 발생하면 해당 에러와 관련 에러 코드를 발생한다.
- 이 많은 에러들을 다 알고 있을 필요는 없지만 이번에는 이 에러 코드를 정리해 해보겠다.
- 주로 많이 발생하는 에러는 404, 500 에러 이다.
HTTP 에러코드 | 에러 메시지 |
100 | Continue |
101 | Switching Protocols |
200 | OK, 에러 없이 전송 성공 |
202 | Accepted, 서버가 클라이언트의 명령을 받음 |
203 | Non-authoritative Information, 서버가 클라이언트 요구 중 일부만 전송함 |
204 | Non Content, 클라이언트 요구를 처리했으나 전송할 데이터가 없음 |
205 | Reset Content |
206 | Partial Content |
300 | Multiple Choices, 최근에 옮겨진 데이터를 요청함. |
301 | Moved Permanently, 요구한 데이터를 변경된 임시 URL에서 찾음 |
302 | Moved Permanently, 요구한 데이터가 변경된 URL에 있음 |
303 | See Other, 요구한 데이터를 변경하지 않았기 때문에 문제가 있음 |
304 | Not modified |
305 | Use Proxy |
400 | Bad Request, 요청 실패 - 문법상 오류가 있어서 서버가 요청 사항을 이해하지 못함. |
401.1 | Unauthorized, 권한 없음 - 접속 실패, 이 에러는 서버에 로그온 하려는 요청 사항이 서버에 들어있는 권한과 비교했을 시 맞지 않을 경우 발생. 이 경우, 요청한 자원에 접근할 수 있는 권한을 부여받기 위해서 서버 운영자에게 요청해야 함. |
401.2 | Unauthorized, 권한 없음 - 서버 설정으로 인한 접속 실패, 이 에러는 서버에 로그온 하려는 요청사항이 서버에 들어있는 권한과 비교했을 때 맞지 않을 경우 발생. 이것은 일반적으로 적절한 www-authenticate head field를 전송하지 않아서 발생함. |
402.3 | Unauthorized, 권한 없음 - 자원에 대한 ACL에 기인한 권한 없음. 이 에러는 클라이언트가 특정 자원에 접근할 수 없을 때 발생. 이 자원은 페이지가 될 수도 있고, 클라이언트의 주소 입력란에 명기된 파일일 수도 있다. 또한, 클라이언트가 해당 주소로 접속할 때 이용되는 또 다른 파일일 수도 있다. 접근할 전체 주소를 다시 확인해 보고 웹 서버 운영자에게 여러분이 자원에 접근할 권한이 있는지를 확인한다. |
401.4 | Unauthorized, 권한 없음 - 필터에 의한 권한 부여 실패. 이 에러는 웹 서버가 서버에 접속하는 사용자들을 확인하기 위해 설치한 필터 프로그램이 있음을 의미함. 서버에 접속하는데 이용되는 인증 과정이 이런 필터 프로그램에 의해 거부된 것임 |
404.5 | Unauthorized, 권한 없음 - ISA PI/CGI 어플리케이션에 의한 권한 부여 실패. 이 에러는 이용하려는 웹 서버의 어드레스에 ISA PI나 CGI 프로그램이 설치되어 있어 사용자의 권한을 검증함. 서버에 접속하는데 이용되는 인증 과정이 이 프로그램에 의해 거부됨. |
402 | Payment Required, 예약됨 |
403.1 | Forbidden, 금지 - 수행 접근 금지. 이 에러는 CGI나 ISA-PI, 혹은 수행시키지 못하도록 되어 있는 디렉터리 내의 실행 파일을 수행시키려고 했을 때 발생함. |
403.2 | Forbidden, 금지 - 읽기 접근 금지. 이 에러는 브라우저가 접근한 디렉터리에 가용한 디폴트 페이지가 없을 경우에 발생함. |
403.4 | Forbidden, 금지 - SSL 필요. 이 에러는 접근하려는 페이지가 SSL로 보안 유지되고 있는 것일 때 발생. |
403.5 | Forbidden, 금지 - SSL 128이 필요. 이 에러는 접근하려는 페이지가 SSL로 보안 유지되고 있는 것일 때 발생. 브라우저가 128비트의 SSL을 지원하는지를 확인해야 함. |
403.6 | Forbidden, 금지 - IP 주소 거부됨. 이 에러는 서버가 사이트에 접근이 허용되지 않은 IP주소로 사용자가 접근하려 했을 때 발생함. |
403.7 | Forbidden, 금지 - 클라이언트 확인 필요. 이 에러는 접근하려는 자원이 서버가 인식하기 위해서 브라우저에게 클라이언트 SSL을 요청하는 경우 발생함. 자원을 이용할 수 있는 사용자임을 입증하는데 사용됨. |
403.8 | Forbidden, 금지 - 사이트 접근 거부. 이 에러는 웹 서버가 요청사항을 수행하고 있지 않거나, 해당 사이트에 접근하는 것을 허락하지 않았을 경우에 발생함. |
403.9 | Forbidden, 금지 - 연결된 사용자수 과다. 이 에러는 웹 서버가 busy한 상태에 있어서 요청을 수행할 수 없을 경우에 발생함. |
403.10 | Forbidden, 금지 - 설정이 확실하지 않음. 이 에러는 웹 서버의 설정 부분에 문제가 있을 경우 발생함. |
403.11 | Forbidden, 금지 - 패스워드 변경. 이 에러는 사용자 인증 단계에서 잘못된 패스워드를 입력했을 경우 발생함. |
403.12 | Forbidden, 금지 - Mapper 접근 금지. 이 에러는 클라이언트 인증용 맵(map)이 해당 웹 사이트에 접근하는 것을 거부할 경우에 발생. |
404 | Not Found, 문서를 찾을 수 없음 - 이 에러는 클라이언트가 요청한 문서를 찾지 못한 경우에 발생함. URL을 다시 잘 보고 주소가 올바로 입력되었는지를 확인함. |
405 | Method not allowed, 메소드 허용 안 됨 - 이 에러는 Request 라인에 명시된 메소드를 수행하기 위한 해당 자원의 이용이 허용되지 않았을 경우에 발생함. |
406 | Not Acceptable, 받아들일 수 없음 - 이 에러는 요청 사항에 필요한 자원은 요청 사항으로 전달된 Accept header에 따라 "Not Acceptable" 내용을 가진 사항이 있을 경우에 발생함. |
407 | Proxy Authentication Required, Proxy 인증이 필요함 - 이 에러는 해당 요청이 수행되도록 proxy 서버에게 인증을 받아야 할 경우에 발생함. |
408 | Request timeout, 요청 시간이 지남 |
409 | Conflict |
410 | Gone, 영구적으로 사용할 수 없음. |
411 | Length Required |
412 | Precondition Failed, 선결조건 실패 - 이 에러는 Request-header filed에 하나 이상에 선결 조건에 대한 값이 서버에서의 테스트 결과 false로 나왔을 경우에 발생 |
413 | Request entity too large |
414 | Request-URI too long, 요청한 URI가 너무 김 - 이 에러는 요청한 URI의 길이가 너무 길어서 서버가 요청 사항의 이행을 거부했을 경우 발생 |
415 | Unsupported media type |
500 | Internal Server Error, 서버 내부 오류 - 이 에러는 웹 서버가 요청사항을 수행할 수 없을 경우에 발생함 |
501 | Not Implemented, 적용 안 됨 - 이 에러는 웹 서버가 요청사항을 수행하는데 필요한 기능을 지원하지 않는 경우에 발생 |
502 | Bad gateway, 게이트웨이 상태 나쁨 - 이 에러는 게이트웨이 상태가 나쁘거나 서버의 과부하 상태일 때 발생한다. |
503 | Service Unavailable, 서비스 불가능 - 이 에러는 서비스가 현재 멈춘 상태 또는 현재 일시적인 과부하 또는 관리 상황일 때 발생될 수 있다. |
504 | Gateway timeout |
505 | HTTP Version Not Supported |
출처: http://hyeonstorage.tistory.com/97
타임리프 문법 및 표현방법 정리 (0) | 2019.05.07 |
---|---|
JAR 파일에 JSP 파일 넣는 방법 (0) | 2016.08.23 |
Html, Mybitis, Ibitis 특수문자 태크정리 (0) | 2016.06.22 |
ext js 환경 (0) | 2016.06.03 |
CSS 란 (0) | 2014.11.18 |
웹 관련 라이브러리를 만들다보면, UI(HTML, JSP, Javascript, css 등) 파일을 함께 배포하고 싶을 때가 있습니다. 즉, 비밀번호 등록 기능 뿐 아니라, 비밀번호 등록 화면까지도 라이브러리에 넣고 싶은 경우가 일 예가 되겠습니다.
그런데, Java 라이브러리는 Jar파일로 만들어야 할 터인데, 화면은 보통 JSP로 만들고…
이것을 어떻게 배포해야 할까요?
META-INF folder를 이용하면 됩니다.
META-INF folder 밑에 resources라는 folder를 만들고, 그 밑에 css, javascript, jsp, html 등을 넣어 jar로 묶어 배포하면 됩니다.
jar 파일의 META-INF/resources folder는 web root로 인식되도록 약속되어 있기 때문입니다.
예를 들어, mylib.jar 파일 안에 “META-INF/resources/jsp/main.jsp”라는 파일이 있다면, 브라우저에서
” http://xxxxx.com/jsp/main.jsp ” 라고 입력하면, 브라우저에 jsp 파일 내용이 출력됩니다.
그런데, 요즘에는 Spring을 사용하면서, jsp 파일이 직접 호출되는 것을 막기 위해 “WEB-INF” 밑에 두고 템플릿 엔진을 사용하는 것이 일반화 되었죠. 이것 역시 아래와 같이 jar 파일을 구성하면, jsp 파일을 브라우저에서 직접 호출할 수 없게 됩니다.
<jar 파일>/META-INF/resources/WEB-INF/jsp/main.jsp
정리하자면, jar 파일안에 있는 “META-INF/resources”는 이 파일을 라이브러리로 사용하는 Web project의 web root와 동일하게 인식한다는 것입니다.
출처: https://stargatex.wordpress.com/2015/10/15/jar-%ED%8C%8C%EC%9D%BC%EC%97%90-jsp-%ED%8C%8C%EC%9D%BC-%EB%84%A3%EB%8A%94-%EB%B0%A9%EB%B2%95/
타임리프 문법 및 표현방법 정리 (0) | 2019.05.07 |
---|---|
HTTP 에러 (0) | 2016.08.29 |
Html, Mybitis, Ibitis 특수문자 태크정리 (0) | 2016.06.22 |
ext js 환경 (0) | 2016.06.03 |
CSS 란 (0) | 2014.11.18 |