React

 

* 리덕스 참조 :

 

1)React Redux 적용

2) React - 생활코딩 4 - Redux , 글 생성 , 수정,삭제 구현 Object.Assign 깊은 복사

3 )React 따라하며 배우는 노드, 리액트 시리즈(John Ahn) - 3 Redux (리덕스 ), 로그인 , 회원 가입 , 인증 체크

 

 

vscode  확장 패키지 추가

1. - Auto Import - ES6, TS, JSX, TSX

2. - Reactjs code snippets

3. - ESLint

4. - Prettier - Code formatter

Visual Studio Code 폴더/파일 아이콘 변경하기

 

 

리액트  프로젝트 생성

 npx create-react-app  경로

예) npx create-react-app E:\react-app2

 

 

* 리액트 리덕스에 필요한 패키지 설치

 

1.리덕스 설치
$ yarn  add redux react-redux

 

2. redux-logger 설치
& yarn add redux-logger

 

3. 크롬 웹스토어에서 redux-devtool  설치
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

 


4. redux-devtools-extension 설치
$  yarn add redux-devtools-extension

redux-devtools 사용법 및 설정 (구글 검색  redux extension devtools)
https://github.com/zalmoxisus/redux-devtools-extension

 


5.리덕스에서 비동기 작업을 처리 위해 redux-thunk 설치

$ yarn add redux-thunk
 

 

 

 

소스 :  https://github.com/braverokmc79/react-redux-setting

Toolkit  적용 소스 :  https://github.com/braverokmc79/react-redux-toolkit-setting

 

1. 리액트 리덕스 (Redux) 튜토리얼 순한맛 1/2

 

 

 

 

 

 

store.js

const redux = require('redux');
const reuxLogger = require('redux-logger');
const createStore = redux.legacy_createStore;
const applyMiddleware = redux.applyMiddleware;
const logger = reuxLogger.createLogger();
const combineReducers = redux.combineReducers;

//action
//action-type
const ADD_SUBSCRIBER = 'ADD_SUBSCRIBER';
const ADD_VIEWCOUNT = 'ADD_VIEWCOUNT';

const addSubscriber = () => {
    return {
        type: ADD_SUBSCRIBER
    }
}

const addViewCount = () => {
    return {
        type: ADD_VIEWCOUNT
    }
}


//reducers
const subscribeState = {
    subscribers: 356
}
const subscriberReducer = (state = subscribeState, action) => {
    switch (action.type) {
        case ADD_SUBSCRIBER:
            return {
                ...state,
                subscribers: state.subscribers + 1
            }
        default:
            return state;
    }
}



const viewState = {
    viewCount: 100
}

const viewReducer = (state = viewState, action) => {
    switch (action.type) {
        case ADD_VIEWCOUNT:
            return {
                ...state,
                viewCount: state.viewCount + 1
            }
        default:
            return state;
    }
}

const rootReducer = combineReducers({
    view: viewReducer,
    subscriber: subscriberReducer,
})



//store
const store = createStore(rootReducer, applyMiddleware(logger));

console.log("store 정보 :", store);

//subscribe -view - dispatch
// console.log("1.store : ", store.getState());
// store.dispatch(addSubscriber());
// console.log("2.store : ", store.getState());


// store.subscribe(() => {
//     console.log('subscribe ===>>', store.getState());
// });

store.dispatch(addSubscriber());
store.dispatch(addSubscriber());
store.dispatch(addSubscriber());
store.dispatch(addViewCount());
store.dispatch(addViewCount());










 

출력=>

store 정보 : {
  dispatch: [Function (anonymous)],
  subscribe: [Function: subscribe],
  getState: [Function: getState],
  replaceReducer: [Function: replaceReducer],
  '@@observable': [Function: observable]
}
 action ADD_SUBSCRIBER @ 20:08:55.279
   prev state { view: { viewCount: 100 }, subscriber: { subscribers: 356 } }
   action     { type: 'ADD_SUBSCRIBER' }
   next state { view: { viewCount: 100 }, subscriber: { subscribers: 357 } }
 action ADD_SUBSCRIBER @ 20:08:55.282
   prev state { view: { viewCount: 100 }, subscriber: { subscribers: 357 } }
   action     { type: 'ADD_SUBSCRIBER' }
   next state { view: { viewCount: 100 }, subscriber: { subscribers: 358 } }
 action ADD_SUBSCRIBER @ 20:08:55.283
   prev state { view: { viewCount: 100 }, subscriber: { subscribers: 358 } }
   action     { type: 'ADD_SUBSCRIBER' }
   next state { view: { viewCount: 100 }, subscriber: { subscribers: 359 } }
 action ADD_VIEWCOUNT @ 20:08:55.284
   prev state { view: { viewCount: 100 }, subscriber: { subscribers: 359 } }
   action     { type: 'ADD_VIEWCOUNT' }
   next state { view: { viewCount: 101 }, subscriber: { subscribers: 359 } }
 action ADD_VIEWCOUNT @ 20:08:55.286
   prev state { view: { viewCount: 101 }, subscriber: { subscribers: 359 } }
   action     { type: 'ADD_VIEWCOUNT' }
   next state { view: { viewCount: 102 }, subscriber: { subscribers: 359 } }

 

 

 

 

 

 

 

 

 

 

 

 

 

2. 리액트 리덕스(Redux)실습강좌 ( 프로젝트에 넣기)

 

 

 

 

1.리덕스 설치
 

$ yarn  add redux react-redux

 

 

 

2.구독 리덕스 만들기

 

 

1)  src 디렉토리 안에  redux 디렉토리  생성후   redux\subscribers     디렉토리를 생성한다.

types.js  파일 성 후 구독 추가 삭제 상수 설정 

src\redux\subscribers\types.js

export const ADD_SUBSCRIBER = 'ADD_SUBSCRIBER';
export const REMOVE_SUBSCRIBER = 'REMOVE_SUBSCRIBER';

 

 

 

2) action  생성

src\redux\subscribers\actions.js

import { ADD_SUBSCRIBER, REMOVE_SUBSCRIBER } from './types';

export const addSubscriber = () => {
    console.log("1.action - addSubscriber:");
    return {
        type: ADD_SUBSCRIBER
    }
}

export const removeSubscriber = () => {
    console.log("1.action - removeSubscriber:");
    return {
        type: REMOVE_SUBSCRIBER
    }
}

 

 

3) reducer  생성 

src\redux\subscribers\reducer.js

import { ADD_SUBSCRIBER, REMOVE_SUBSCRIBER } from './types';

const initialState = {
    count: 370
}
const subscriberReducer = (state = initialState, action) => {
    console.log("2. action.type :", action.type);
    switch (action.type) {

        case ADD_SUBSCRIBER:
            return {
                ...state,
                count: state.count + 1
            }
        case REMOVE_SUBSCRIBER:
            return {
                ...state,
                count: state.count - 1
            }

        default:
            return state;
    }
}

export default subscriberReducer;


 

 

4) store 생성

src\redux\store.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import subscriberReducer from './subscribers/reducer';
const store = createStore(subscriberReducer );

export default store;

 

 

5) App.js  설정

store 정보를 가져와서  Provider store={store}   로    App  전체를 감싸준다. 

src\App.js

import './App.css';
import store from './redux/store';
import { Provider } from 'react-redux';
import Subscribers from './components/Subscribers';
import Display from './components/Display';

function App() {
  return (
    <Provider store={store}>
      <div className="App">
        <Subscribers /> 
        <Display />      
      </div>
    </Provider>
  );
}

export default App;

 

 

 

6) 컴포넌트에서 리덕스 사용 방법

components 디렉토리 생성후  Subscribers.js 컴포넌트 생성한다.

공식 문서 : https://react-redux.js.org/api/connect

 

src\components\Subscribers.js

import React from 'react'
import { connect } from 'react-redux';
import { addSubscriber, removeSubscriber } from "../redux/subscribers/actions";


const Subscribers = ({ count, addSubscriber, removeSubscriber }) => {
    return (
        <div className='items'>
            <h2>Subscribers 컴포넌트 구독자 수:{count}</h2>
            <button onClick={() => addSubscriber()}>구독 하기!</button>
            <button onClick={() => removeSubscriber()}>구독 취소하기!</button>
        </div>
    )
}


//공식 문서 : 설정
//https://react-redux.js.org/api/connect

const mapStateToProps = (state) => {
    //console.log(state, 'state');
    return {
        count: state.count
    }
}


//함수 방식
const mapDispatchToProps = (dispatch) => {
    return {
        addSubscriber: () => dispatch(addSubscriber()),
        removeSubscriber: () => dispatch(removeSubscriber())
    }
}

//객체방식
// const mapDispatchToProps = {
//     addSubscriber,
//     removeSubscriber
// }


//Subscribers 컴포넌트를 export 하기 전에 mapStateToProps , 와 mapDispatchToProps 연결 설정을한다.
export default connect(mapStateToProps, mapDispatchToProps)(Subscribers)

 

src\components\Display.js

import React from 'react'
import { connect } from 'react-redux';

const Display = (props) => {
    return (
        <div>
            <p>Display 컴포넌트  구독자 수 : {props.count}</p>
        </div>
    )
}

const mapStateToProps = (state) => {
    return {
        count: state.count
    }
}
export default connect(mapStateToProps)(Display);

 

 

 

 

 

 

3. 조회수 리덕스 만들기 

 

1)src\redux\views\types.js

export const ADD_VIEW = "ADD_VIEW";

 

2)src\redux\views\actions.js

import { ADD_SUBSCRIBER, REMOVE_SUBSCRIBER } from './types';

export const addSubscriber = () => {
    console.log("1.action - addSubscriber:");
    return {
        type: ADD_SUBSCRIBER
    }
}

export const removeSubscriber = () => {
    console.log("1.action - removeSubscriber:");
    return {
        type: REMOVE_SUBSCRIBER
    }
}

 

3)src\redux\views\reducer.js

import { ADD_SUBSCRIBER, REMOVE_SUBSCRIBER } from './types';

const initialState = {
    count: 370
}
const subscriberReducer = (state = initialState, action) => {
    console.log("2. action.type :", action.type);
    switch (action.type) {

        case ADD_SUBSCRIBER:
            return {
                ...state,
                count: state.count + 1
            }
        case REMOVE_SUBSCRIBER:
            return {
                ...state,
                count: state.count - 1
            }

        default:
            return state;
    }
}

export default subscriberReducer;



 

4) App.js 에서  <Display /> 임포트

 

 

 

 

 

 

4. 컴포넌트의 리덕스 import  from 값을 줄이기.

1)src\redux\index.js  파일 생성

export { addSubscriber, removeSubscriber } from "./subscribers/actions";
export { addView } from "./views/actions";

 

2)컴포넌트에서 다음과 같이 값을 줄여 줄수 있다.

src\components\Subscribers.js

//import { addSubscriber, removeSubscriber } from "../redux/subscribers/actions";
import { addSubscriber, removeSubscriber } from "../redux";

 

 

 

 

5. reudcer 들을 하나로 묶으기   combineReducers

 

1) 다음과 같이 reducer 들만 가져와서 하나로 묶어 줄수 있다.

src\redux\rootReducer.js

import { combineReducers } from 'redux';
import subscriberReducer from './subscribers/reducer';
import viewReducer from './views/reducer';

const rootReducer = combineReducers({
    subscribers: subscriberReducer,
    views: viewReducer
});

export default rootReducer;

 

2)store.js 설정

src\redux\store.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
//import subscriberReducer from './subscribers/reducer';
import rootReducer from './rootReducer';

//const store = createStore(subscriberReducer );
const store = createStore(rootReducer);


export default store;

 

3)컴포넌트에서 다음과 같이 사용한다

 src\components\Subscribers.js

~
~

//공식 문서 : 설정
//https://react-redux.js.org/api/connect
//rootReducer.js 에 설정한 combine 설정값을 가져온다. 
// const rootReducer = combineReducers({
//     subscribers: subscriberReducer,
//     views: viewReducer
// });
const mapStateToProps = ({ subscribers }) => {
    //console.log(state, 'state');
    return {
        count: subscribers.count
    }
}

~

 

 

 

 

 

 

6. redux-logger    및  redux-devtools-extension 설치

1) redux-logger 설치

$ yarn add redux-logger

 

 

2) 크롬 웹스토어에서 redux-devtool  설치
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

 


3) redux-devtools-extension 설치

$  yarn add redux-devtools-extension

 

redux-devtools 사용법 및 설정 (구글 검색  redux extension devtools)
https://github.com/zalmoxisus/redux-devtools-extension
 

 

4) store  설정

src\redux\store.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
//import subscriberReducer from './subscribers/reducer';

//rootReducer combine 적용
import rootReducer from './rootReducer';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';

//1.기본
//const store = createStore(rootReducer);

//2.logger 사용시
//const store = createStore(rootReducer, applyMiddleware(logger));


//3.logger && composeWithDevTools 사용시
const middleware = [logger]
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)));


export default store;

 

 

 

 

 

 

7. 리덕스에서 비동기 작업을 처리 위해 redux-thunk 설치

1) 설치

$ yarn add redux-thunk

 

2) store  에서 redux-thunk 임포트 후  middleware 에 추가만 해주면 된다.

src\redux\store.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
//import subscriberReducer from './subscribers/reducer';

//rootReducer combine 적용
import rootReducer from './rootReducer';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

//1.기본
//const store = createStore(rootReducer);

//2.logger 사용시
//const store = createStore(rootReducer, applyMiddleware(logger));


//3.logger && composeWithDevTools 사용시
const middleware = [logger, thunk]
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)));

export default store;

 

 

3)액션을 동기식 처리를 위한 코딩 이미지와같이 Comments.js 컴포넌트 파일과   redux comments  파일을 생성한다.

thunk 가 있음므로서 action 에서 dispatch 가 가능해 진다.

 

 jsonplaceholder 더미데이터

https://jsonplaceholder.typicode.com/comments

 

 

 

4) src\reux\comments\types.js

export const FETCH_COMMENTS = "FETCH_COMMENTS";
export const FETCH_COMMENTS_REQUEST = "FETCH_COMMENTS_REQUEST";
export const FETCH_COMMENTS_SUCCESS = "FETCH_COMMENTS_SUCCESS";
export const FETCH_COMMENTS_FAILURE = "FETCH_COMMENTS_FAILURE";

 

5) src\reux\comments\actions.js

import { FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE } from "./types"

//thunk 사용 설정으로 dispatch 함수 설정이 가능함
export const fetchComments = () => {
    return (dispatch) => {
        //동기식으로 순차적으론 진행 가능.
        dispatch(fetchCommentRequest());

        fetch("https://jsonplaceholder.typicode.com/comments")
            .then(res => res.json())
            .then(comments =>
                dispatch(fetchCommentSuccess(comments))
            )
            .catch(error => dispatch(fetchCommentFailuer(error)));
    }
}

export const fetchCommentRequest = () => {
    return {
        type: FETCH_COMMENTS_REQUEST
    }
}

export const fetchCommentSuccess = (comments) => {
    return {
        type: FETCH_COMMENTS_SUCCESS,
        payload: comments
    }
}

export const fetchCommentFailuer = (error) => {
    return {
        type: FETCH_COMMENTS_FAILURE,
        payload: error
    }
}

 

 

6) src\reux\comments\reducer.js

import { FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE } from "./types"

const initialState = {
    items: [],
    loading: false,
    err: null
}

const commentsReducer = (state = initialState, action) => {
    switch (action.type) {
        case FETCH_COMMENTS_REQUEST:
            return {
                ...state,
                location: true
            }
        case FETCH_COMMENTS_SUCCESS:
            return {
                ...state,
                items: action.payload,
                location: false
            }
        case FETCH_COMMENTS_FAILURE:
            return {
                ...state,
                err: action.payload,
                location: false
            }
        default: return state;
    }
}

export default commentsReducer;

 

 

 


 

7)src\redux\index.js

export { addSubscriber, removeSubscriber } from "./subscribers/actions";
export { addView } from "./views/actions";
export { fetchComments } from "./comments/actions";

 

 

 

8)src\redux\rootReducer.js

import { combineReducers } from 'redux';
import subscriberReducer from './subscribers/reducer';
import viewReducer from './views/reducer';
import commentsReducer from './comments/reducer';

const rootReducer = combineReducers({
    subscribers: subscriberReducer,
    views: viewReducer,
    comments: commentsReducer
});

export default rootReducer;

 

 

 

9) src\components\Comments.js

import React, { useEffect } from 'react'
import { connect } from 'react-redux';
import { fetchComments } from '../redux';


const Comments = ({ fetchComments, loading, comments }) => {

    useEffect(() => {
        fetchComments();
    }, [])

    const commentsItems = loading ? (<div>is loading...</div>) : (
        comments.map(comment => (
            <div key={comment.id}>
                <h3>{comment.name}</h3>
                <p>{comment.email}</p>
                <p>{comment.body}</p>
            </div>
        ))
    );


    return (
        <div className='comments'>
            {commentsItems}
        </div>
    )
}

const mapStateToProps = ({ comments }) => {
    return {
        comments: comments.items,
        loading: comments.loading
    }
}

const mapDispatchToProps = {
    fetchComments
}

export default connect(mapStateToProps, mapDispatchToProps)(Comments);

 

 

10)App.css

.App {
  text-align: center;
}

.items{
  border-bottom:1px solid #333;
  margin-bottom: 1rem;
  padding-bottom: 1rem;

}
.comments{
  display: grid;
  grid-template-columns: repeat(4,1fr);
  grid-gap: 1rem;
}

.comments  > div{
  border: 1px solid #333;
}

 

11)App.js

Comments  임포트

import './App.css';
import store from './redux/store';
import { Provider } from 'react-redux';
import Subscribers from './components/Subscribers';
import Display from './components/Display';
import Views from './components/Views';
import Comments from './components/Comments';

function App() {
  return (
    <Provider store={store}>
      <div className="App">
        <Comments />
        <Subscribers />
        <Views />
        <Display />
      </div>
    </Provider>
  );
}

export default App;

 

 

 

실행

 

 

 

 

 

 

 

 

3. 옛날 리덕스를 최신 리덕스 Toolkit으로 바꿔보자!

 

 

https://redux-toolkit.js.org/introduction/getting-started

 

1. 리덕스 Toolkit  다음 4가지를 한방에 처리해 준다.

 

①combinereducer

② thunk

③applyMiddleware

④composeWithDevTools

 

소스 :  https://github.com/braverokmc79/react-redux-toolkit-setting

 

◆  기존 코드             =>                   리덕스 Toolkit 적용 코드

 

 

 

 

1 ) reducer 에서 적용 방법

기존 코드

redux/comments/reducer.js

import { FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE } from "./types"

const initialState = {
    items: [],
    loading: false,
    err: null
}

const commentsReducer = (state = initialState, action) => {
    switch (action.type) {
        case FETCH_COMMENTS_REQUEST:
            return {
                ...state,
                location: true
            }
        case FETCH_COMMENTS_SUCCESS:
            return {
                ...state,
                items: action.payload,
                location: false
            }
        case FETCH_COMMENTS_FAILURE:
            return {
                ...state,
                err: action.payload,
                location: false
            }
        default: return state;
    }
}

export default commentsReducer;

 

types 을 만들어 줄 필요가 없다.

전개 연산 복사를 할필요가 없다.

default 처리를 할 필요가 없다.

name: "commentsReducer", name 임의 고유 아이디값 입력한다.

 

===> 리덕스 Toolkit  처리로  다음과 같이 변경한다.

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    items: [],
    loading: false,
    err: null
}

const commentsSlice = createSlice({
    name: "commentsReducer",
    initialState,
    reducers: {

        fetchCommentsRequest(state, action) {
            state.location = true
        },
        fetchCommentsSuccess(state, action) {
            state.items = action.payload;
            state.location = false;
        },
        fetchCommentsFailure(state, action) {
            state.err = action.payload;
            state.location = false;
        }
    }
});


export const commentsActions = commentsSlice.actions;
export default commentsSlice.reducer;

 

 

 

 

2 ) actions 에서 적용 방법

기존 코드

src/redux/comments/actions.js

import { FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE } from "./types"

//thunk 사용 설정으로 dispatch 함수 설정이 가능함
export const fetchComments = () => {
    return (dispatch) => {
        //동기식으로 순차적으론 진행 가능.
        dispatch(fetchCommentRequest());

        fetch("https://jsonplaceholder.typicode.com/comments")
            .then(res => res.json())
            .then(comments =>
                dispatch(fetchCommentSuccess(comments))
            )
            .catch(error => dispatch(fetchCommentFailuer(error)));
    }
}

export const fetchCommentRequest = () => {
    return {
        type: FETCH_COMMENTS_REQUEST
    }
}

export const fetchCommentSuccess = (comments) => {
    return {
        type: FETCH_COMMENTS_SUCCESS,
        payload: comments
    }
}

export const fetchCommentFailuer = (error) => {
    return {
        type: FETCH_COMMENTS_FAILURE,
        payload: error
    }
}

 

types 이 필요없고 redux/comments/reducer.js 에서 export 한 

 commentsActions  을 import 후   함수값만 호출하면  된다.

더이상 object 로 type 과 payload  만들어서 전달할 필요없이  함수에 매개변수로 직접 넣으면 된다.

 

 

===>  리덕스 Toolkit  처리로  다음과 같이 변경한다.

import { commentsActions } from './reducer';

export const fetchComments = () => {
    return (dispatch) => {

        dispatch(commentsActions.fetchCommentsRequest());

        fetch("https://jsonplaceholder.typicode.com/comments")
            .then(res => res.json())
            .then(comments =>
                dispatch(commentsActions.fetchCommentsSuccess(comments))
            )
            .catch(error => (
                dispatch(commentsActions.fetchCommentsFailure(error))
            ));
    }
}


 

 

3 ) store  에서 적용 방법

①combinereducer

② thunk

③applyMiddleware

④composeWithDevTools

===> toolkit 으로 한방에 처리

 

기존 store.js 와 combineReducers  처리인 rootReducer.js 코드를  하나의 파일로  간단하게 변경처리 가능하다.

redux/store.js

import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import rootReducer from './rootReducer';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';

const middleware = [logger, thunk]
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)));

export default store;

 

redux/rootReducer.js

import { combineReducers } from 'redux';
import subscriberReducer from './subscribers/reducer';
import viewReducer from './views/reducer';
import commentsReducer from './comments/reducer';

const rootReducer = combineReducers({
    subscribers: subscriberReducer,
    views: viewReducer,
    comments: commentsReducer
});

export default rootReducer;

 

 

 

===>리덕스 Toolkit  처리로  다음과 같이 변경한다.

redux/store.js

import { configureStore } from "@reduxjs/toolkit";
import subscriberReducer from './subscribers/reducer';
import viewReducer from './views/reducer';
import commentsReducer from './comments/reducer';

const store = configureStore({
    reducer: {
        subscribers: subscriberReducer,
        views: viewReducer,
        comments: commentsReducer
    }
})

export default store;

 

 

 

4) 컴포넌트에서는  기존과 동일하게 사용하면된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

적게 아는 사람은 보통 말을 많이 하고, 아는 사람은 말을 적게 한다. -루소

댓글 ( 4)

댓글 남기기

작성