Search

'분류 전체보기'에 해당되는 글 291건

  1. 2020.12.20 connect
  2. 2020.12.17 react router
  3. 2020.12.16 redux
  4. 2020.12.14 error: internal/modules/cjs/loader.js:883
  5. 2020.12.13 visual code definition 찾고 돌아가기
  6. 2020.12.13 scss
  7. 2020.12.13 webpack
  8. 2020.12.11 localStorage
  9. 2020.12.11 Props
  10. 2020.12.11 state
  11. 2020.12.06 item decoration
  12. 2020.12.06 babel
  13. 2020.12.06 arrow function
  14. 2020.12.05 Realm
  15. 2020.12.05 CoreData
  16. 2020.12.05 UserDefaults
  17. 2020.11.30 IQKeyboardManager
  18. 2020.11.30 URLSession
  19. 2020.11.30 Pod
  20. 2020.11.24 애드핏 광고 등록
  21. 2020.11.24 코드 한 줄 짜는데 시간이 오래 걸린다 (1)
  22. 2020.11.24 tableview
  23. 2020.11.23 codable
  24. 2020.11.23 extension
  25. 2020.11.23 closure
  26. 2020.11.23 segue
  27. 2020.11.21 protocol
  28. 2020.11.21 Delegate
  29. 2020.11.14 Camera2로 전면 카메라 보이는대로 녹화하기 - 2
  30. 2020.11.08 Camera2로 전면 카메라 보이는대로 녹화하기 - 1

connect

개발/react 2020. 12. 20. 11:08 Posted by 아는 개발자

connect 함수는 리액트 앱의 하위 컴포넌트에서 redux store를 접근하는 것을 가능하게 해주는 역할을 한다. 이 함수를 이용해서 컴포넌트 단에서 redux store에 접근하고 액션을 호출 할 수 있게 된다. 이번 포스트에서는 간단한 예제로 connect 함수를 통해 redux store를 사용하는 방법을 다뤄보려고 한다.

 

0. 준비작업

 

connect 함수 소개를 위해 예제와 텍스트와 숫자를 담당하는 redux를 만들었다.

 

BlogStore.js

import { createStore, combineReducers } from 'redux';

const textReducerState = {
    text: '',
    name: 'textReducer'
};

const textReducer = (state = textReducerState, action) => {
    switch (action.type) {
        case 'SET_TEXT':
            return {
                ...state,
                text: action.text
            };
        default: 
            return state;
    }
}

const numberReducerState = {
    numberState: 30,
    name: 'numberReducer'
};

const numberReducer = (state = numberReducerState, action) => {
    switch (action.type) {
        case 'SET_NUMBER':
            return {
                ...state,
                number: action.number
            };
        default: 
            return state;
    }
};

export const configureStore = () => {
    const store = createStore(
        combineReducers({
            text: textReducer,
            number: numberReducer
        })
    );
    return store;
};

 

BlogActions.js

 

export const setText = (
    text = ''
) => ({
    type: 'SET_TEXT',
    text
});

export const setNumber = (
    number = 0
) => ({
    type: 'SET_NUMBER',
    number
});

 

 

1. Provider 

 

configureStore() 함수를 통해 store를 생성하고 Provider 태그에 store를 속성값으로 넣는다. 이러면 하위에 추가되는 component에서 redux store를 바라볼 수 있는 창구가 만들어진다.

 

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { configureStore } from './BlogStore';
import BlogMain from './BlogMain';

const store = configureStore();


const jsx = (
    <Provider store={store}>
        <BlogMain />
    </Provider>
);


ReactDOM.render(jsx, document.getElementById('app'));

 

2. connect 

 

하위 컴포넌트 단에서는 Provider에서 제공하는 store 함수를 connect 함수를 통해서 받아온다. 함수 형식이든 클래스 형식이든 받는 방식은 동일하다. 

 

 

2.1 클래스형식

 

import React from 'react';
import { connect } from 'react-redux';
import BlogDetail from './BlogDetail';


class BlogMain extends React.Component {
    render() {
        console.log(this.props.text)
        console.log(this.props.number)
        return (
            <div>
                <p>BlogMain component</p>
                <BlogDetail />
            </div>
        )
    };
};
const mapStateToProps = (state) => {
    return {
        text: state.text,
        number: state.number
    }
};
export default connect(mapStateToProps)(BlogMain);

 

클래스 형식 컴포넌트를 export 할 때 connect 함수를 사용하고 첫번째 인자에 mapStateToProps 함수를 넣었는데 redux store에 있는 값을 컴포넌트에 어떻게 넘겨줄지 세팅하는 작업이다.  넘겨 받은 값은 component의 props에 들어가서 호출 할 수 있다. 아래 사진은 render() 함수 안에서 console로 찍은 로그다. textReducer와 numberReducer가 출력되는 것을 볼 수 있다.

 

 

2.2 함수 형식 

 

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

const BlogDetail = (props) => (
    <div>
        <p>BlogTextDetail</p>
        <p>{props.text.name}</p>
    </div>
);
const mapStateToProps = (state) => {
    return {
        text: state.text
    };
};

export default connect(mapStateToProps)(BlogDetail)

 

함수 형식도 크게 다르지 않다. 컴포넌트 내에서 호출 할 때 this를 부르지 않아도 된다는 점만 다르다. 위 코드로 호출하면 아래 그림처럼 화면 뷰가 그려진다.

 

 

3. Action 

 

connect로 컴포넌트에 전달 할 때 store만 전달 하는것이 아니라 action을 넣을 수 있는 dispatch 함수까지 전달된다. react 디바이스 툴을 사용해보면 component의 props 안에 dispatch가 들어있는 것을 확인 할 수 있다. 

 

 

실제로도 잘 사용할 수 있을 지 테스트 해보자. 방금 전에 사용한 BlogDetail 컴포넌트를 살짝 수정해서 현재 store에 저장된 text를 출력하고 버튼을 추가하고 클릭하면 text를 BlogDetail로 바뀌도록 했다.

 

import React from 'react';
import { connect } from 'react-redux';
import { setText } from './BlogActions';

const BlogDetail = (props) => (
    <div>
        <p>BlogTextDetail</p>
        <p>current store text value: {props.text.text}</p>
        <button onClick={() => {
            props.dispatch(setText('BlogDetail'))
        }}>Change text to BlogDetail</button>
    </div>
);

const mapStateToProps = (state) => {
    return { text: state.text};
};

export default connect(mapStateToProps)(BlogDetail)

 

함수를 실행하고 버튼을 클릭하니 화면이 아래와 같이 store의 text 값이 BlogDetail로 변경됐다.

 

 

4. 총평 

 

코딩을 처음 하는 분이면 이걸 왜 이렇게까지 해야할지 이해가 안될 수 있을 것 같은데 이전에 mvvm, mvc 패턴을 경험해본 개발자들에게는 redux가 크게 어려울 것 같지 않다. 강의 들을 때는 헷갈렸는데 실제로 코드로 짜보니까 어떤 식으로 구조를 잡아야할 지 느낌이 온다. 물론 자바스크립트 언어 특성상 state 세부 이름을 관리할 때 꽤 귀찮음을 겪을 것 같긴 하다.

728x90

'개발 > react' 카테고리의 다른 글

useEffect  (0) 2020.12.21
useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13

react router

개발/react 2020. 12. 17. 19:52 Posted by 아는 개발자

react 에서는 react-router-dom 라이브러리를 통해  들어 오는 주소 별로 별도의 페이지를 보여줄 수 있는 라우팅 기능을 제공한다. 리액트의 특성에 맞게 이 라이브러리는 어떤 페이지로 진입 했을 때 어떤 페이지를 보여 줄 것인지를 컴포넌트 단위로 뽑을 수 있다.

 

1. 라이브러리 임포트 

 

리액트와 필요한 라이브러리를 임포트 한다.

 

import React from 'react';
import { BrowserRouter, Route, Switch, Link, NavLink } from 'react-router-dom';

 

2. Route

 

const AppRouterExample = () => (
    <BrowserRouter>
        <div>
            <Switch>
                <Route path="/" component={() => (<h2>This is DashboardPage</h2>)} exact={true} />
                <Route path="/create" component={() => (<h2>This is Create Page</h2>)} exact={false} />
                <Route path="/edit/:id" 
                    component={ (props) => (<h2>This is Edit Page {props.match.params.id}</h2>)} 
                        exact={true} />
                <Route path="/help" 
                    component={() => (<h2>This is help page</h2>)} 
                    exact={true} />
                <Route component={() => (<h2>This is not 404 page</h2>)} />
            </Switch>
        </div>
    </BrowserRouter>
);

 

<BrowserRouter> 와 <Switch> 태그 내에 위치한 <Route> 태그로 관리하고 싶은 경로를 설정할 수 있다. 이렇게 두면 애플리케이션이 관리하는 경로를 설정 할 수 있게 된다.

 

2.1 path 

 

라우팅할 경로를 정의하는 값이다. 위 페이지에서는 /create, /edit, /help 페이지를 경로로 뒀다. edit 페이지의 경우에는 수정하려는 데이터의 id를 인자로 받을 수 있고 이 값은 component에 전달된다. component 객체에 props로 전달되며 저장되는 필드는 컴포넌트에 있는 값과 같다.

 

2.2 component 

 

해당 경로로 들어올 경우 어떤 component를 보여줄 것인지를 결정하는 곳이다. 직접 컴포넌트를 만들어서 넣을 수 있으며 이 예제에서는 component 필드 내에서 보여줄 수 있는 값을 넣었다. /edit 경로를 보면 props로 Route로부터 인자를 받아오는데 /edit에서 받아오는 id 정보를 확인 할 수 있다.

 

2.3 exact 

 

exact는 이 경로를 명확하게 볼 것인지 말것인지를 설정한다. 평소 익히 쓰던 path와 다른 개념이라 와닿지 않을 것 같은데 exact값이 false면 앞에 부분만 맞아도 해당 페이지로 렌딩이 된다. 예로 /create 는 exact 값이 false이기 때문에 /create/34 로 접근하든, /create/edit 으로 접근하든 모두 create 페이지로 렌딩해준다.

 

3. NavLink 

 

const AppRouterExample = () => (
    <BrowserRouter>
        <div>
            <div>
                <h1>This is Header</h1>
                <NavLink to="/" activeClassName="is-active" exact={true}>Dashboard</NavLink>
                <NavLink to="/create" activeClassName="is-active">Create Expense</NavLink>
                <NavLink to="/edit" activeClassName="is-active">Edit Expense</NavLink>
                <NavLink to="/help" activeClassName="is-active">Help</NavLink>
            </div>

 

NavLink는 하이퍼링크 기능이고 스타일링을 가능하게 한다. 내부에 있는 필드 값을 바꿔서 더 링크를 더 이쁘게 만들 수 있다.

728x90

'개발 > react' 카테고리의 다른 글

useState  (0) 2020.12.21
connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13
webpack  (0) 2020.12.13

redux

개발/react 2020. 12. 16. 20:28 Posted by 아는 개발자

react는 state를 이용해서 컴포넌트의 상태를 관리하는데 state 하나에 들어가는 element가 많아질수록 관리하기가 힘들어지는 문제가 있다. 그래서 react에서는 redux라는 라이브러리를 이용해서 state를 좀더 쉽게 관리할 수 있게 해줬다. 크게 state를 관리하는 store와 store 를 변경하려는 dispatch 그리고 변경 작업을 일괄 관리하는 reducer로 나뉘는데 이번 포스트에서는 각각에 대해서 간단히만 다뤄보려고 한다.

 

1. createStore 

 

redux로 관리할 state 집합을 만드는 작업이다. 함수의 인자로는 reducer를 받는다. 

 

const store = createStore(countReducer);

 

2. action 

 

state를 바꾸기 위해 취하는 액션이다. 단 여기에는 액션이 들어가지 않고 액션에 필요한 데이터만 들어간다. 변수 이름을 자유롭게 입력할 수 있는데 type 필드에 액션의 종류를 정해주는게 일반적인 것 같다. dispatch 함수를 통해 액션을 실행 할 수 있다.

 

const incrementCount = ({incrementBy = 1} = {}) => ({
    type: 'INCREMENT',
    incrementBy
});

store.dispatch(incrementCount())

 

3. reducer 

 

액션을 대신 처리해주는 부분이다. MVC로 치면 controller에 해당하는 부분으로 실제 state 값의 변경을 담당한다. reducer에서 state에 대한 변경을 일괄적으로 담당하기 때문에 관리하기가 한결 쉬워지는 장점이 있다.

 

// Reducer
// 1. Reducers are pure functions
// 2. Never change state or action 

const countReducer = (state = { count: 0 }, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return {
                count: state.count + action.incrementBy
            };
        default:
            return state;
    }
};

 

4. subscribe 

 

store의 subscribe 함수로 state 변경에 대한 변화를 구독 할 수 있다. reducer에서 업데이트 값들은 모두 여기를 거치게 된다. state 변경 값을 화면에 노출해야하는 경우에는 이 함수를 사용하면 된다.

 

store.subscribe(() => {
    console.log(store.getState());
});
728x90

'개발 > react' 카테고리의 다른 글

connect  (0) 2020.12.20
react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13
webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11

error: internal/modules/cjs/loader.js:883

개발/삽질 기록 2020. 12. 14. 09:58 Posted by 아는 개발자

리액트 프로젝트를 복사해서 다시 사용하는데 이런 에러가 발생했다면. 

 

internal/modules/cjs/loader.js:883

 

이전에 사용한 프로젝트의 node_modules에서 만든 경로랑 꼬여서 생긴 문제일 확률이 높다. 이럴때는 모듈 별로 수정하는 방법이 있겠지만 간단하게 node_modules 폴더를 모두 날리고 다시 설치하는게 빠르다. 

 

rm -rf node_modules
rm -f package-lock.json
yarn cache clean
yarn install
728x90

visual code definition 찾고 돌아가기

개발/삽질 기록 2020. 12. 13. 16:59 Posted by 아는 개발자

visual code로 작업을 하다보면 어떤 클래스의 정의를 찾고 나서 이전 페이지로 돌아가고 싶은 경우가 있다. Jetbrain계열 IDE만 사용하다 visual code로 들어와서 헤맸는데 이번 포스트에 간단히 정리한다. 거의 게임체인저 급의 단축키임이 틀림 없다.

 

Mac 기준

 

정의 찾기: command + 마우스 클릭  

 

이전 페이지로 돌아가기: control + -

 

Window 기준

 

정의찾기: control + 마우스 클릭

 

이전 페이지로 돌아가기: Alt + 왼쪽 방향키

728x90

'개발 > 삽질 기록' 카테고리의 다른 글

cannot find module - heroku  (0) 2021.01.10
error: internal/modules/cjs/loader.js:883  (0) 2020.12.14
visual code definition 찾고 돌아가기  (0) 2020.12.13
RxJava: mapper function returned null 에러  (0) 2020.02.14
addr2line  (0) 2018.12.22
gcc로 pthread API 컴파일하기  (2) 2018.10.30

scss

개발/react 2020. 12. 13. 15:50 Posted by 아는 개발자

Syntactically Awesome StylesheetS 의 준말로 기존 css보더 더 편하게 스타일링 할 수 있는 언어다. css에 있는 모든 기능을 포함하고 여기에 더해 변수랑 내부에서 제공하는 함수도 사용할 수 있기 때문에 개발자들의 반복적인 작업을 확 줄여준다. 5년 전에 간단히 홈페이지 만들면서 css를 쓰고 불편하다고 느낀후 지금까지 손댄적이 없었는데 지금은 scss 라는 언어로 더 쉽게 스타일링 할 수 있게 된 것 같다. 이번포스트에서는 scss를 react에 적용하는 방법을 간단히 소개한다.

 

1. scss 로더 추가 

 

scss는 확장 기능이므로 새로 css로 transformation 해주는 기능이 필요하다. 별도의 로더를 설치하고 스크립트를 실행할 수 있으나 여기선 webpack에 로더를 추가해서 정리하려고 한다. 먼저 아래 명령어로 필요한 라이브러리를 추가한다.

 

yarn add style-loader css-loader sass-loader node-sass

 

그리고 webpack.config.js에 css, scss 용 로더를 추가한다.

 

module.exports = {
    ...
    module: {
        rules: [{
            loader: 'babel-loader',
            test: /\.js$/,
            exclude: /node_modules/
        }, {
            test: /\.s?css$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]

 

2. scss 파일 추가 

 

scss로 쓸 파일을 추가한다. scss 언어 내에 import 기능이 있어 여러 개의 파일로 의존성이 가능하다. 최상위 파일로 styles.scss 을 만들고 예시로 base 폴더 내에 base.scss 파일을 추가한다.

 

styles.scss

 

@import './base/base';

 

base.scss

 

간단하게 브라우저 전체에 적용될 수 있는 스타일링을 넣었다.

 

body {
    background: $dark-blue;
    font-family: Helvetica, Arial, sans-serif;
    font-size: 2.4rem;
}

 

3. entry 파일에서 임포트 

 

entry에 해당하는 파일에서 아까 추가한 scss 파일을 임포트 한후 간단한 코드를 넣어 봤다.

 

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/styles.scss';

ReactDOM.render(<div><h1>This is selfish-developer blog</h1></div>, document.getElementById('app'))

 

그 결과 아래처럼 스타일링이 된 것을 확인 할 수 있다.

 

728x90

'개발 > react' 카테고리의 다른 글

react router  (0) 2020.12.17
redux  (0) 2020.12.16
scss  (0) 2020.12.13
webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
TAG CSS, REACT, scss

webpack

개발/react 2020. 12. 13. 15:10 Posted by 아는 개발자

webpack 은 자바스크립트로 짠 애플리케이션의 모듈 관리용 툴이다. 애플리케이션에서 필요한 모듈의 의존성을 관리하고 프로젝트용 코드를 만들어준다. 예로 리액트 자바스크립트에서 필요한 babel, style-loader, css-loader 같은 스타일용 로더, 애플리케이션의 시작위치를 정하는 entry, 디버깅용 devtool 설정, 서버 관리까지 모두 webpack을 이용해서 관리가 가능하다. 짜치는 일들을 하나로 묶어서 관리해주는 라이브러리니 배워두면 프로젝트 관리할 때 크게 도움이 될 것 같다.

 

1. Configuration

 

webpack.config.js 파일을 통해서 webpack에서 관리하고 싶은 설정을 추가할 수 있다. 아래 코드는 튜토리얼 프로젝트에서 추가한 webpack 설정 코드다. 공식 문서를 살펴보면 더 많은 설정을 추가 할 수 있다. 이번 포스트에서는 주요 설정에 대해서만 정리하려고 한다.

 

module.exports = {
    entry: './src/app.js',
    output: {
        path: path.join(__dirname,'public'),
        filename: 'bundle.js'
    },
    module: {
        rules: [{
            loader: 'babel-loader',
            test: /\.js$/,
            exclude: /node_modules/
        }, {
            test: /\.s?css$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    },
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        contentBase: path.join(__dirname,'public')
    }
};

 

1.1 Entry 

 

webpack이 어디에서 의존성 그래프를 그려갈 지 설정하는 곳이다. 어디서부터 프로그램을 시작할지 설정하는 작업이라고 봐도 좋다. c언어 프로그램으로 치면 main함수를 어디에 둘 지 설정하는 작업이라고 봐도 될 것 같다. 위 소스코드에서는 src 폴더에 있는 app.js를 시작점으로 두었다.

 

1.2 Output 

 

webpack으로 만든 묶음(bundle)을 어디에 생성할 것인지를 설정한다. 일단 c언어라면 컴파일후 바이너리 결과물을 어디에 둘 것인지 설정하는 것과 같다.

 

1.3 Loaders

 

webpack 이 어떤 타입의 파일을 특정한 모듈로 변경하고 의존성 그래프에 추가할지를 설정하는 작업이다. 위 코드의 module 내부를 보면 test 속성과 use 속성이 있는데 test는 어떤 파일을 변형할 것인지를 설정하고 use는 어떤 loader를 사용할 것인지 설정한다. 여기서 test는 파일을 설정하는 작업이라고 했는데 파일 이름을 정규표현식으로 정하고 있다. 위 코드에서는 jsx 컨버팅용 babel-loader와 css 스타일링용 style-loader, css-loader, sass-loader 를 추가했다.

 

1.4 DevServer 

 

webpack-dev-server 라이브러리를 이용해 빠르게 프로그램을 시작 할 수 있는 기능이다. devServer 값을 설정하면 리액트 프로그램을 시작 하는 주소나 포트번호 같은 값을 쉽게 설정 할 수 있다. 위 코드에서는 가장 기본인 시작 위치만 설정 했다.

 

2. 설치 및 사용

 

2.1 설치 

 

npm을 이용해 global 하게 설치하는 방법도 있지만 꼬여버리면 답이 없으므로 프로젝트 단위로 yarn을 이용해서 설치한다. 

 

yarn add webpack webpack-dev-server

 

2.2 스크립트 파일 추가 

 

package.json에 스크립트 코드를 추가한다

 

{
  "name": "knowing-developer",
  ...
  "scripts": {
    "build": "webpack",
    "dev-server": "webpack-dev-server"
  },

 

build 스크립트는 webpack을 이용해 프로젝트 파일을 변경 시켜주는 스크립트다. 로컬에 서버까지 만드려면 dev-server 스크립트까지 추가한다.

 

2.3 실행 

 

yarn run dev-server

 

방금 추가한 스크립트를 실행한다.

728x90

'개발 > react' 카테고리의 다른 글

redux  (0) 2020.12.16
scss  (0) 2020.12.13
webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11

localStorage

개발/react 2020. 12. 11. 18:00 Posted by 아는 개발자

 

리액트의 localStorage는  Android의 SharedPreference나 iOS의 UserDefaults 처럼 애플리케이션 단위로 key-value 값을 저장할 수 있는 라이브러리다. 아래 코드를 보면 getItem 함수에 key값을 넣어서 값을 받아오고 setItem 함수에 key 값을 넣어서 값을 업데이트한다. 

 

아래 코드를 보면 JSON 라이브러리를 사용해 변환하는 과정이 있는데 이 이유는 localStorage에서는 string의 형태로만 저장이 가능하기 때문이다. 그래서 일반 텍스트를 사용하는 것이 아니면 모두 JSON을 이용해 값을 변환해서 저장해야한다. 배열 같은 값을 저장한다면 어차피 변환하는 과정이 필요하기 때문에 항상 써두는 것도 나쁘지 않을 것 같다.

 

class App extends React.Component {
    constructor(props) {
        super(props);
        this.handleIncrease = this.handleIncrease.bind(this)
        this.handleDecrease = this.handleDecrease.bind(this)


        this.state = {
            counter: 0
        };
    }

    componentDidMount() {
        const json = localStorage.getItem('counter');
        const counter = JSON.parse(json);
        
        if (counter) {
            this.setState((prevState) => {return {counter: counter}})
        }
    }

    componentDidUpdate(prevProps, prevState) {
        const json = JSON.stringify(this.state.counter);
        localStorage.setItem('counter', json)
    }

 

componentDidMount() 와 componentDidUpdate() 함수는 리액트의 라이프사이클 관리 함수다. componentDidMount()는 리액트 컴포넌트가 처음 생성 될 때 호출되고 componentDidUpdate는 리액트 컴포넌트에 변화가 생겼을 때, 예로 state 값이 변화해서 UI가 바뀌었을 때 호출된다. 위 코드는 각 라이프 사이클에서 필요한 작업을 넣은 것이다. 주로 componentDidMount에서 기존 값을 가져오고(fetch) componentDidUpdate에서 값을 업데이트 한다

 

소스코드

 

https://github.com/kwony/react-study/blob/main/src/playground/blog-localstorage.js

728x90

'개발 > react' 카테고리의 다른 글

scss  (0) 2020.12.13
webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11
babel  (0) 2020.12.06

Props

개발/react 2020. 12. 11. 18:00 Posted by 아는 개발자

props는 리액트에서 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달 할 때 사용하는 기능이다. 아래 그림처럼 Header가 App 안에 포함되어 있을 때 App에서 Header에게 title과 subtitle을 전달하게 되는데 이를 props를 이용해서 할 수 있다.

 

 

1. 데이터 전달

 

class App extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        const title = '아는개발자'
        const subtitle = '리액트를 공부해봅시다'
        return (
            <div>
                <Header title={title} subtitle={subtitle} />
            </div>
        )
    }
}

 

상위 컴포넌트의 render() 함수에서 하위 컴포넌트를 리턴할 때 내부에 key와 value로 전달한다. Header 컴포넌트에 subtitle로 전달하고 있다. key 값은 자유롭게 설정할 수 있고 받는 곳에서 동일한 key 값으로만 받아오면 된다.

 

2. 데이터 받아오기 

 

class Header extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return(
            <div>
                <h1>{this.props.title}</h1>
                <h2>{this.props.subtitle}</h2>
            </div>
        )        
    }
}

 

상위로부터 받아온 props 데이터를 확인해본다. props 데이터는 {this.props} 내에 있고 상위에서 보내준 key 값에 따라서 값을 확인 할 수 있다.

 

3. stateless function 

 

함수의 형태에서도 props로 전달된 값을 받아 올 수 있다. 아래 코드에선 props 변수를 받는 함수를 만들고 return 값으로 props에서 받아온 이름으로 버튼을 만들었다. 상위 컴포넌트인 App에서는 Action 컴포넌트에 buttonName을 props로 전달했다. 

 

const Action = (props) => {
    return (
        <div>
            <button>
                {props.buttonName}
            </button>
        </div>
    )
}

class App extends React.Component {
    ...
    render() {
    ...
                <Action buttonName={'click'} />
            </div>
        )
    }
}

 

4. 함수 전달

 

props로 데이터 뿐만 아니라 함수도 전달 할 수 있다. App 컴포넌트에 알럿 메시지를 띄우는 handleClick이라는 함수를 만들고 생성자에서 바인딩을 한 다음 Action 컴포넌트에 데이터를 전달하는 것과 동일하게 함수를 전달한다. 버튼을 그리는 Action 함수에서는 button 의 버튼 클릭 콜백함수에 받아온 handleClick을 인자로 넣는다. 

 

class App extends React.Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this)
    }

    handleClick() {
        alert('button clicked');
    }

    render() {
        const title = '아는개발자'
        const subtitle = '리액트를 공부해봅시다'
        const buttonName = 'click'
        return (
            <div>
                <Header title={title} subtitle={subtitle} />
                <Action buttonName={buttonName} handleClick={this.handleClick} />
            </div>
        )
    }
}

const Action = (props) => {
    return (
        <div>
            <button onClick={props.handleClick}>
                {props.buttonName}
            </button>
        </div>
    )
}

 

테스트 결과 버튼을 클릭하면 App에서 설정한 알럿 메시지가 띄워지는것을 확인 할 수 있다.

 

 

5. 소스코드

 

https://github.com/kwony/react-study/blob/main/indecision-app/src/playground/blog-props.js

728x90

'개발 > react' 카테고리의 다른 글

webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11
babel  (0) 2020.12.06
arrow function  (0) 2020.12.06
TAG Props, REACT

state

개발/react 2020. 12. 11. 18:00 Posted by 아는 개발자

state는 리액트 컴포넌트 내에서 사용할 변수를 관리하는 역할을 한다. 예로 간단하게 정수 값을 표시하고 1씩 증가시키고 감소시키는 버튼이 있다고 하자. 아래와 같은 기능을 제공하는 앱이라면 컴포넌트중 누군가는 현재 화면에 표시되는 값을 들고 있어야 한다.

 

1. state 관리

 

이 값은 컴포넌트내의 state 변수에서 관리한다. 아래 코드를 보면 App 컴포넌트의 생성자에서 state를 만들고 그 안에 counter라는 값을 초기화 하는 것을 볼 수 있다. 그리고 render() 함수에서 현재 state 값을 참조해 counter 값을 보여주고 있다.

 

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            counter: 0
        };
    }

    render() {
        const title = '아는개발자'
        const subtitle = '이번에는 state를 공부해봅시다'
        const incrButton = '+1'
        const decrButton = '-1'
        return (
            <div>
                <Header title={title} subtitle={subtitle} />
                <Action buttonName={incrButton} />
                <Action buttonName={decrButton} />
                <p>현재 값: {this.state.counter}</p>
            </div>
        )
    }
}

 

2. state 변경 

 

state 값을 업데이트 할 때는 constructor 함수에서 처럼 this.state 값에 직접 업데이트 하는게 아니라 setState 함수를 사용한다. state 값이 업데이트 되면서 이 값을 참조하고 있는 ui도 동적으로 업데이트하기 위해서다. 이렇게 하지 않으면 state 값만 바뀌고 실제 화면은 그대로 남게된다. 

 

class App extends React.Component {
    constructor(props) {
        super(props);
        this.handleIncrease = this.handleIncrease.bind(this)
        this.handleDecrease = this.handleDecrease.bind(this)
        this.state = {
            counter: 0
        };
    }

    handleIncrease() {
        this.setState((prevState) => ({
            counter: prevState.counter + 1
        }))
    }
    handleDecrease() {
        this.setState((prevState) => {return {counter: prevState.counter - 1}})
    }

    render() {
        ...
        const incrButton = '+1'
        const decrButton = '-1'
        return (
            <div>
                ...
                <Action buttonName={incrButton} handleClick={this.handleIncrease} />
                <Action buttonName={decrButton} handleClick={this.handleDecrease} />
                <p>현재 값: {this.state.counter}</p>
            </div>
        )
    }

 

handleIncrease 함수와 handleDecrease 함수에서 setState 내부 구현부를 살짝 다르게 했다. handleIncrease처럼 간단하게 값을 업데이트할 수도 있고 handleDecrease처럼 return까지 포함해서 값을 업데이트 할 수도 있다.

 

3. 소스코드

https://github.com/kwony/react-study/blob/main/src/playground/blog-state.js

 

728x90

'개발 > react' 카테고리의 다른 글

webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11
babel  (0) 2020.12.06
arrow function  (0) 2020.12.06
TAG REACT, State

item decoration

개발/안드로이드 2020. 12. 6. 14:25 Posted by 아는 개발자

recycler view를 사용할 때 item 간의 간격을 다르게 주고 싶을 때가 있다. 예를 들어 a타입과 b타입의 아이템 사이의 간격은 상하 10dp, b타입과 c타입의 간격은 상하 5dp 이런식으로 설정하거나 더 보편적으로는 마지막 아이템인 경우에는 간격을 좀 더 띄워서 넣으려고 하는 경우가 있다. 이때 가장 빠르게 떠오르는 방법은 recyclerview의 adapter에서 position별로 margin을 주는 경우인데 이렇게 하면 안된다. recyclerview에서 자체적으로 position을 관리하기 때문에 내가 보고 있는 recyclerview에서 관리하고 있는 position이 다르다. 그래서 나는 분명히 제대로 준것 같은데 실제로 보면 다른 item에 margin이 들어간다. 이 부분이 크게 눈에 띄지 않는 부분이라 잘못 짜두고도 눈치채기가 어려워 종종 그냥 넘어가는데 나중에 디버깅해보면 item간의 간격이 내가 의도한 것과 다르게 표시된다. 그것도 아주 보기 싫게.

 

item간의 간격을 dynamic하게 조절할 때는 recycler view에서 관리하는 item decoration 라이브러리를 사용해야한다. 여기서 넘어오는 view는 recycler view에서 관리하고 있는 현재 item의 view다. 이 인자와 getChildAdapterPosition 함수를 이용해 현재 view item의 index를 찾을 수 있다. 이 정보와 outRect 인자를 활용해서 각 간격을 얼마나 줄 것인지 설정 할 수 있다.

 

recyclerview.addItemDecoration(object: RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        when (parent.getChildAdapterPosition(view)) {
            0 -> {
                outRect.left = DimensionUtils.dp2px(context, 20f).toInt()
                outRect.right = DimensionUtils.dp2px(context, 10f).toInt()
            }
            listAdapter?.itemCount?: 1 - 1 -> {
                outRect.left = DimensionUtils.dp2px(context, 10f).toInt()
                outRect.right = DimensionUtils.dp2px(context, 20f).toInt()
            }
            else -> {
                outRect.left = DimensionUtils.dp2px(context, 10f).toInt()
                outRect.right = DimensionUtils.dp2px(context, 10f).toInt()
            }
        }
    }
})

 

이제 잘못짠 코드들을 하나씩 수정해야겠다..

728x90

babel

개발/react 2020. 12. 6. 14:12 Posted by 아는 개발자

babel은 JFX로 작성된 react javascript 파일을 브라우저에서 인식할 수 있도록 변경해주는 컴파일러다. 예시로 아래처럼 생긴 코드를 브라우저에 렌더링 하려고 하면

 

const appRoot = document.getElementById('app');

function renderApp() {
    // JSX - JavaScript XML 
    var template = <div>
        <p>Hello react</p>
    </div>

    var appRoot = document.getElementById('app');
    ReactDOM.render(template, appRoot);
 }
 renderApp()

 

요런 에러가뜬다.

 

이건 JFX로 작성된 형태를 브라우저에서 읽을 수 없기 때문이다. react에서 기본적으로 만들어주는 프로젝트를 사용하면 이런 에러가 뜨지 않는데 처음부터 만들어가면 요런 에러를 보게 된다. babel 라이브러리를 이용하면 JFX로 작성한 언어를 컴파일해서 브라우저가 읽을 수 있는 형태로 바꿀 수 있다.

 

먼저 아래 명령어를 사용해서 babel을 설치한다.

 

npm install babel-cli

 

아래 명령어로 컴파일을 할 수 있다. 이 명령어의 뜻은 source 파일에 있는 build-it-visible.js 라는 파일을 babel로 컴파일해서 결과물을 public/scripts/app.js에 입력하라는 뜻이다. presets 옵션은 컴파일 옵션이고, watch는 build-it-visible.js 파일의 변화를 계속 관찰하겠다는 뜻이다. 저장하면 자동으로 컴파일을 해주므로 유용하다.

 

babel src/build-it-visible.js --out-file=public/scripts/app.js --presets=env,react --watch

 

그래서 아까 파일을 babel로 컴파일해주면 요렇게 바뀌게 된다.

 

'use strict';

var appRoot = document.getElementById('app');

function renderApp() {
    // JSX - JavaScript XML 
    var template = React.createElement(
        'div',
        null,
        React.createElement(
            'p',
            null,
            'Hello react'
        )
    );

    var appRoot = document.getElementById('app');
    ReactDOM.render(template, appRoot);
}
renderApp();

 

그리고 브라우저에서는 요렇게 잘 뜨게 된다.

 

 

사실 Babel은 인터프리터로 봐야한다.

728x90

'개발 > react' 카테고리의 다른 글

webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11
babel  (0) 2020.12.06
arrow function  (0) 2020.12.06

arrow function

개발/react 2020. 12. 6. 13:59 Posted by 아는 개발자

요즘 트렌드 언어 답게 react에서 사용하는 javascript도 arrow function으로 함수를 줄일 수 있다. 자바에서 사용하는 람다나 swift의 closure 랑 비슷하게 이름없는 함수(anonymous) 를 사용해서 1회성의 함수를 따로 선언하지 않고 삽입해서 쓸 수 있는 기능이다.

 

이 함수들은 모양새는 다르지만 모두 똑같이 제곱값을 리턴하는 함수다.

 

const square1 = function(x) {
    return x * x
};

const square2 = (x) => {
    return x * x;
};

const square3 = (x) => x * x;

 

객체 내에서 함수로 들어갈 때도 동일하게 줄일 수 있다. 

 

const user = {
    name: 'kwony',
    cities: ['pangyo', 'sinchon', 'madrid'],
    printPlacedLived: function() {
        return this.cities.map((city) => {
            return this.name + ' has lived in ' + city + '!';
        });
    },

    printPlacedLived2() {
        return this.cities.map((city) => {
            return this.name + ' has lived in ' + city + '!';
        });
    }
}

 

view 요소의 콜백 함수에도 arrow function을 적용할 수 있다. 아래 코드보면 첫번째 버튼은 arrow function을 적용해서 넣었고 두번째 버튼은 따로 만든 onClickButton 함수를 사용했다. 두개 모두 동일한 역할을 한다.

 

const onClickButton = () => {
    toggle.isVisible = !toggle.isVisible
    render()
}

const render = () => {
    const template = (
        <div>
        <button onClick={() => {
            toggle.isVisible = !toggle.isVisible
            render()
        }}>{toggle.isVisible ? 'hide detail' : 'show detail'}</button>
        <button onClick={onClickButton}>{toggle.isVisible ? 'hide detail' : 'show detail'}</button>

 

간단해보이지만 손에 익으려면 꽤 시간이 걸리니 이것도 놓치지 말고 꼼꼼히 연습해봐야겠다.

728x90

'개발 > react' 카테고리의 다른 글

webpack  (0) 2020.12.13
localStorage  (0) 2020.12.11
Props  (0) 2020.12.11
state  (0) 2020.12.11
babel  (0) 2020.12.06
arrow function  (0) 2020.12.06

Realm

개발/iOS 2020. 12. 5. 15:21 Posted by 아는 개발자

Realm은 오픈소스로 운영되는 모바일용 데이터베이스 클래스다. CoreData 처럼 관계형 데이터베이스를 읽고 쓰는 것을 지원하고있다. 개인적으로 CoreData를 사용하는 것 보다 훨씬 직관적이고 사용하기가 간편하다. 이번 포스트에서는 Realm을 Swift에 사용하는 방법을 간단히 정리해본다. 

 

1. Realm cocoapod 임포트

 

pod 'RealmSwift', '~> 4.4.1'

 

RealmSwift 라이브러리를 임포트시킨다. 몇 ios 버전에선 이전 Realm 버전으로 임포트하면 persmission 에러가 뜬다고 해서 4.4.1 버전으로 임의로 설정했다.

 

2. 클래스 설정 

 

import Foundation
import RealmSwift

class Category: Object {
    @objc dynamic var name: String = ""
    
    let items = List<Item>() 
}

 

Realm의 테이블로 추가할 클래스를 선언한다. Object 클래스를 상속해야하는데 이 클래스가 RealmSwift에 포함돼 있어야하는것을 유의하자. name은 Category 클래스의 이름에 해당하는 값이고 items는 Category 클래스와 연결된(relation) 클래스를 의미한다. Category와 Item이 1:N 의 관계가 될 예정이다.

 

import Foundation
import RealmSwift

class Item: Object {
    @objc dynamic var title: String = ""
    @objc dynamic var done: Bool = false
    @objc dynamic var dateCreated: Date?
    var parentCategory = LinkingObjects(fromType: Category.self, property: "items")
}

 

Category 클래스에 포함될 Item 클래스를 선언한다. 아래 parentCategory에 LinkingObjects 를 활용하면 쉽게 둘간의 관계를 정해줄 수 있다.

 

3. CRUD 

 

3.1 Create 

 

let newCategory = Category()
newCategory.name = textField.text!

save(category: newCategory)

func save(category: Category) {
    let realm = try! Realm()
    do {
        try realm.write {
            realm.add(category)
        }
    } catch {
        print("Error saving contet \(error)")
    }
    
    self.tableView.reloadData()
}

 

생성하는 작업은 CoreData랑 비슷하다. 임의의 Category 클래스를 만들고 realm 라이브러리로 추가해주는 작업을 하면 된다. realm.write 함수의 콜백 내에 add 함수를 추가하면 된다. 

 

if let currentCategory = self.selectedCategory {
    do {
        try self.realm.write {
            let realm = try! Realm()
            let newItem = Item()
            newItem.title = textField.text!
            newItem.done = false
            newItem.dateCreated = Date()
            currentCategory.items.append(newItem)
        }

 

Category에  포함된 Item의 경우는 약간 다르다. 아래 코드를 보면 Category처럼 add 함수로 추가하는게 아니라 포함되어있는 Category 클래스의 items 리스트에 append 시켜서 추가하고 있다. 서로 linking되어 있어서 그렇다.

 

3.2 READ 

 

func loadCategoreis() {
    let realm = try! Realm()
    self.categories = realm.objects(Category.self)
    
    self.tableView.reloadData()
}

 

읽어오는 작업은 realm 클래스에서 objects 함수에 읽어오려는 클래스를 추가하면 된다. 이러면 간단히 읽어올 수 있게 된다. 

 

3.3 UPDATE 

 

if let item = todoItems?[indexPath.row] {
    do {
        try realm.write {
            item.done = !item.done

 

realm으로부터 읽어온 클래스를 realm.write 콜백내에서 값을 수정하기만 하면 된다.

 

3.4. DELETE 

 

if let item = todoItems?[indexPath.row] {
    do {
        try realm.write {
            realm.delete(item)

 

수정과 마찬가지로 realm으로 읽어온 클래스를 realm.write 콜백내에서 삭제하면 된다.

 

4. 총평 

 

다른 개발자들도 CoreData가 정말 쓰기 불편했나보다. realm을 찬양하는 개발자들이 더 많다. 그리고 성능도 realm이 coredata에 비해서 압도적으로 훌륭하다. 물론 앱에서 데이터를 저장할 때는 이정도로 중요할것 같지는 않지만 말이다.

 

728x90

'개발 > iOS' 카테고리의 다른 글

Realm  (0) 2020.12.05
CoreData  (0) 2020.12.05
UserDefaults  (0) 2020.12.05
IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30

CoreData

개발/iOS 2020. 12. 5. 13:16 Posted by 아는 개발자

CoreData는 iOS 플랫폼 단에서 지원하는 관계형 데이터베이스 라이브러리다. XCode에서 지원하는 툴과 CoreData를 사용하면 SQL의 어려운 쿼리문을 사용하지 않고도 테이블을 만들고 데이터를 추가할 수 있다. 이번 포스트에서는 Xcode를 이용해 CoreData 를 초기화하고 기본 CRUD 작업을 수행하는 것을 다뤄보려고 한다.

 

1. Data Model 파일 생성 및 초기화

 

CoreData를 사용하려면 Data Model 타입의 파일을 하나 생성해야한다. New File을 클릭하고 필터로 data 를 입력해서 Data Model 타입의 파일을 하나 추가한다.

 

이 작업이 완료되면 AppDelegate.swift 파일에 아래 코드를 추가해야한다. 현재 앱에서 바라보고 있는 데이터 모델을 세팅해주고 앱이 꺼지기 전에 데이터를 저장하는 콜백을 등록하는 작업이다. 여기서 추가한 변수인 persistentContainer는 나중에 ViewController에서 여기서 선언한 변수를 사용할 예정이다. 

 

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
	... 
    
    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        self.saveContext()
    }

    lazy var persistentContainer: NSPersistentContainer = {
    
        let container = NSPersistentContainer(name: "DataModel")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()
    
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }

 

2.  Entity, Attribute 추가 

 

아까 생성했던 DataModel 파일을 클릭해보면 왼쪽 하단에 Add Entitiy 라는 버튼이 있다. 현재 앱에서 관리할 테이블을 추가하는 과정이라고 봐도 될 것 같다. 클릭하면 Entity라는 이름으로 새로운 Entity가 생성된다. 용도에 맞게 이름을 변경해준다. 나는 Item이라고 정했다.

 

 

Entity를 만들고 나면 오른쪽 하단에 Add Attribute 버튼이 있는데 Entity가 가지게 될 속성을 추가하는 작업이다. SQL로 생각하면 테이블에 컬럼을 추가하는 것과 같다. 앱을 만들며 필요하다고 생각한 칼럼 값을 여기에 추가하면 된다. Item Entity에 name이라는 속성을 String 타입으로 추가했다. 오른쪽 패널에 이 속성에 특정 값들을 설정 할 수 있다. Optional이면 null을 허용한다는 뜻이고 Derived는 상속을 받는다? 인것 같다. 이거는 구상한 스키마에 따라서 결정하면 될 것 같다.

 

속성을 모두 업데이트 하면 Entity의 오른쪽 패널을 통해 Entity에 대한 속성을 설정 할 수 있다. Module은 Entity로부터 파생되는 Class가 사용되는 모듈을 설정하는데 특별한 경우가 아니면 현재 프로덕트 모듈(Current Product Module)로 변경해준다. Codegen은 Entity가 필요한 클래스를 자동 생성할지(Class Definition) 아니면 커스텀하게 생성할 지 (Category/Extension)을 결정하는 작업이다. Category/Extension을 사용하는 경우 직접 클래스를 만들어야하기 때문에 아직 CoreData를 공부하는 입장에서는 Class Defnition을 먼저 사용해보는게 좋다.

 

 

3. CRUD 

 

3.1 CREATE

 

DataModel 작업을 마치면 ViewController에서 아까 만든 Entity를 직접 데이터로 추가하는 작업이 남았다.  먼저 새로운 Entity를 추가하는 코드를 보자. 아래 코드는 Alert view에서 Item의 이름을 받아와 새로 추가하는 작업이다. 코드 중간부분을 보면 newItem 이라는 새로운 변수를 추가했는데 생성자로 사용한 클래스가 Item이다. 이 Item 클래스는 이전에 Data Model에서 추가한 Entity랑 이름이 같다. 이 생성자에서는 context라는 인자를 받는데 AppDelegate 클래스에서 선언한 persistentContainer 변수의 속성인 viewContext를 상용한다. 그다음 context.save() 함수를 호출해서 이 값을 통해 Item 변수를 새롭게 추가할 수 있다. 이것만 해주면 된다. SQL처럼 insert 어쩌고 저쩌고 쿼리를 날릴 필요가 없어서 간편하다.

 

@IBAction func addButtonPressed(_ sender: Any) {
    
    var textField = UITextField()
    
    let alert = UIAlertController(title: "Add New Todoey Item", message: "", preferredStyle: .alert)
    
    let action = UIAlertAction(title: "Add Item", style: .default) { (action) in
        // what will happend once user clicks the Add Item button on our UIAlert
        
        let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
        let context = container.viewContext
        let newItem = Item(context: context)
        newItem.name = textField.text!
        self.itemArray.append(newItem)
        
        do {
            try context.save()
        } catch {
            print("Error saving contet \(error)")
        }

 

3.2 READ 

 

DataModel에서 만든 Entity에는 모두 fetchRequest() 라는 함수가 있는데 이 함수는 Entity에 해당하는 데이터를 읽어올 수 있는 select Request 쿼리를 갖고 있다. 따로 변수로 만들어서 생성 작업과 동일하게 context를 불러오고 fetch 함수 내에 request를 인자로 넣으면 데이터를 읽어올 수 있다.

func loadItems() {
    let request: NSFetchRequest<Item> = Item.fetchRequest()
    
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    
    do {
        itemArray = try context.fetch(request)
    } catch {
        print("error fetching data from context \(error)")
    }

 

3.3 UPDATE 

 

수정작업은 CoreData로부터 받아온 클래스의 속성 값을 수정하고 save를 호출하는 작업으로 이뤄진다. 이것 또한 복잡한 쿼리를 넣을 필요 없이 간단히 수정이 가능하다.

 

func updateItem(item: Item) {
    item.name = "update name"
    
    do {
        try context.save()
    } catch {
        print("error fetching data from context \(error)")
    }

 

3.4 DELETE 

 

삭제도 크게 다르지 않다. CoreData로부터 받아온 클래스를 context.delete의 인자로 넣어주고 save를 호출한다. 이렇게만 하면 데이터가 삭제된다.

 

func delete(item: Item) {
    context.delete(item)
    
    do {
        try context.save()
    } catch {
        print("error fetching data from context \(error)")
    }

 

4. 총평 

 

복잡한 쿼리를 짤 필요가 없어서 간편하긴 한데 Xcode 특유의, 마우스를 활용한 작업이 많아서 그런가 익숙해지려면 시간이 좀 걸릴 것 같은 느낌이다. 속성같은 것들은 키보드로 설정하는게 더 간편하고 직관적인데 말이다.

728x90

'개발 > iOS' 카테고리의 다른 글

Realm  (0) 2020.12.05
CoreData  (0) 2020.12.05
UserDefaults  (0) 2020.12.05
IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30

UserDefaults

개발/iOS 2020. 12. 5. 12:31 Posted by 아는 개발자

앱을 개발하다 보면 종종 단일의 데이터를 저장해야하는 경우가 생긴다. 예로 들면 어떤 가이드 화면을 보여줬는지 안보여줬는지 유무를 저장하는 Boolean 타입의 데이터나 영상의 음량을 미리 정해두는 Float 타입의 데이터값 같은 것들이 있다. 이런 데이터들은 관계형 데이터베이스로 저장하는 것 보다는 key - value로 저장하는게 효율적인데 iOS에서는 UserDefaults라는 라이브러리를 이용해 이 기능을 제공한다. 안드로이드를 경험한 개발자들은 SharedPreference 클래스와 비슷한 역할을 한다고 보면 될 것 같다. 사용하는 방법도 비슷하고 간편하다.

 

class TodoListViewController: UITableViewController {
    
    let defaults = UserDefaults.standard
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        defaults.setValue(false, forKey: "booleanKey")
        defaults.setValue(123, forKey: "integerKey")
        defaults.bool(forKey: "booleanKey")
        defaults.integer(forKey: "integerKey")

 

값을 업데이트 할 때는 setValue 함수로 value와 key값을 넣고 읽을 때는 불러오려는 데이터의 타입형의 함수에 key 값을 넣어서 호출한다. UserDefaults 함수로 초기화만 잘 해주면 돼서 사용하는데 큰 어려움은 없다.

728x90

'개발 > iOS' 카테고리의 다른 글

Realm  (0) 2020.12.05
CoreData  (0) 2020.12.05
UserDefaults  (0) 2020.12.05
IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30

IQKeyboardManager

개발/iOS 2020. 11. 30. 11:07 Posted by 아는 개발자

 

iOS에서도 소프트 키보드를 토글하면 UI 뷰 화면을 덮어버리는 문제가 있었다. 안드에서는 그래도 activity 단에서 키보드가 뜰때 어떻게 UI 레이아웃을 변형할지 어느정도 조정이 가능한데 ios에서는 그런게 특별히 없는 것 같다. 키보드가 뜰 때 마다 OS에 물어봐서 keyboard의 height를 알아오고 그에 맞춰서 View를 올려줘야 한다고 하는데.. 매번 하기엔 꽤 번거로운 일이다. 

 

다행히 삽질(?)을 미리 해두고 라이브러리 형태로 배포를 해뒀다고 한다. 라이브러리 이름은 IQKeyboardManager 이고 사용방법도 아주 간단하다. 먼저 cocoapod을 이용해 라이브러리를 임포트 하자.

 

platform :ios, '13.0'

target 'Flash Chat iOS13' do
  use_frameworks!

  pod 'IQKeyboardManagerSwift'

end

 

임포트후 AppDelegate라는 파일에 IQKeyboardManager 클래스를 호출해서 관련 속성을 세팅하자.

 

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        IQKeyboardManager.shared.enable = true
        IQKeyboardManager.shared.enableAutoToolbar = false
        IQKeyboardManager.shared.shouldResignOnTouchOutside = true
        
        return true
    }

 

이렇게만 하고 다시 실행하면 keyboard가 올라옴에 따라서 UI가 자동으로 이동하는 것을 확인 할 수 있다.

 

728x90

'개발 > iOS' 카테고리의 다른 글

CoreData  (0) 2020.12.05
UserDefaults  (0) 2020.12.05
IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30
tableview  (0) 2020.11.24

URLSession

개발/iOS 2020. 11. 30. 10:42 Posted by 아는 개발자

JAVA나 C, C++과 마찬가지로 swift 언어의 라이브러리를 이용해서 외부 서버랑 통신을 주고 받을 수 있다. 절차도 비슷한데

 

1. 먼저 통신할 주소를 URL 클래스로 생성하고

2. URLSession 을 만든 다음 

3. URLSession에 아까 만든 주소로 task를 할당하고 응답시 처리할 코드를 입력하고

4. task를 실행한다.

 

func performRequest(_ urlString: String) {
    // 1. Create a URL
    
    if let url = URL(string: urlString) {
        // 2. Create a URL session
        
        let session = URLSession(configuration: .default)
        
        // 3. Give the session a task
        
        let task = session.dataTask(with: url) { (data, response, error) in
            if  error != nil {
                self.delegate?.didFailWithError(error!)
                return
            }
            
            if let safeData = data {
                if let weather = self.parseJSON(safeData) {
                    self.delegate?.didUpdateWeather(self, weather)
                }
            }
        }
        
        // 4. Start the task
        task.resume()
    }
}
728x90

'개발 > iOS' 카테고리의 다른 글

UserDefaults  (0) 2020.12.05
IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30
tableview  (0) 2020.11.24
codable  (0) 2020.11.23

Pod

개발/iOS 2020. 11. 30. 10:32 Posted by 아는 개발자

안드로이드에서 build.gradle 파일을 수정해 파이어베이스나 외부 깃허브 라이브러리를 다운 받을 수 있었던 것처럼 Xcode에서는 cocoapod과 swiftpackage 라는 툴로 이런 기능을 제공하고 있는데 . swiftpackage는 비교적 최근에 애플에서 만들었고 사용하기도 간편하지만 보편화되지 않아서 아직까지는 cocoapod을 상요하는 추세라고 한다. 이번 포스트에서는 cocoapod을 사용해서 외부 라이브러리를 임포트 하는 방법을 다뤄보려고 한다.

 

1. cocoapod 설치하기 

 

맥북에 cocoapod이 설치되지 않았다면 아래 명령어로 터미널을 켜서 cocoapod을 먼저 설치한다.

sudo gem install cocoapods
pod setup --verbose

 

2. 프로젝트에 pod 초기화하기 

 

개발중인 Xcode 프로젝트의 최상단 위치에서 아래 명령어를 수행한다. 최상단 위치는 *.xcodeproj 이런 파일이 있는 곳을 말한다.

pod init // 프로젝트 최상위 위치에서

 

실행하고나면 Podfile 이라는 루비 언어로 작성된 파일이 생긴다. 

 

platform :ios, '13.0'

target 'Flash Chat iOS13' do
  use_frameworks!

  # Pods for Flash Chat iOS13
end

 

3. 추가하고 싶은 라이브러리 넣기 

 

프로젝트에서 사용하려는 Third party 라이브러리 스크립트를 Podfile에 추가한다. 스크립트는 사용하려는 프로젝트 라이브러리의 github에서 확인 할 수 있다.

 

platform :ios, '13.0'

target 'Flash Chat iOS13' do
  use_frameworks!

  # Pods for Flash Chat iOS13
  pod 'Firebase/Auth'
  pod 'Firebase/Core'
  pod 'Firebase/Firestore'
  pod 'IQKeyboardManagerSwift'

end

 

4. 설치 스크립트 실행

 

라이브러리를 추가했다면 아래 코드를 실행해서 파일을 받아온다. 

 

pod install

 

5. 새로운 프로젝트 실행 

 

4번의 작업이 끝나면 아래 그림처럼 xcworkdspace 라는 확장자를 가진 파일이 만들어진다. 이 파일을 실행해서 프로젝트를 다시 실행한다. 프로젝트에 Pod을 설정하는 작업이기 때문에 처음 Pod을 초기화 할 때만 해주면 된다. 

 

728x90

'개발 > iOS' 카테고리의 다른 글

IQKeyboardManager  (0) 2020.11.30
URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30
tableview  (0) 2020.11.24
codable  (0) 2020.11.23
extension  (0) 2020.11.23

애드핏 광고 등록

사이드 프로젝트/이기적인 총무 2020. 11. 24. 14:25 Posted by 아는 개발자

옛날에는 구글 애드센스로만 광고를 넣을 수 있었는데 요새는 Dable, Tenting 처럼 다양한 플랫폼에서도 블로그나 앱에 광고를 넣을 수 있게됐다. 그중 주변에 나랑 비슷하게 사이드 프로젝트를 진행중이신 분들이 카카오의 애드핏을 많이 추천하셔서 나도 내가 옛날에 열심히 만든 이기적인 총무애 애드핏으로 광고를 넣어봤다. 훌륭한 개발자가 많은 회사라 그런지(?) 넣는 과정은 어렵지 않고 깔끔했는데 이번 포스트에선 애드핏으로 Android 앱 광고를 넣는 방법을 소개해보려고 한다. 

 

1. 애드핏 라이브러리 임포트 

 

애드핏에선 안드로이드에 광고를 넣기 위한 별도의 라이브러리를 만들어줬다. 이 라인들을 추가해서 새로운 라이브러리를 임포트하자.

 

// app 단위 build.gradle

+ implementation 'androidx.ads:ads-identifier:1.0.0-alpha04'
+ implementation "com.kakao.adfit:ads-base:3.5.2"


// project 단위 build.gradle 

allprojects {
    repositories {
        mavenCentral()
        jcenter()
        + maven { url 'http://devrepo.kakao.com:8088/nexus/content/groups/public/' }

 

2. 인터넷 및 보안 설정 

 

AndroidManifest 파일 일부 수정이 필요하다. 아직 인터넷을 허용하지 않은 앱이라면 인터넷과 네트워크 상태 접근 권한을 추가하고 application 단에서는 networkSecurityConfig 파일을 설정해준다. 이 파일이 이미 있다면 아래 network-security-config 옵션을 추가하고 없으면 새롭게 만들어주면 된다. 경로는 아래 코드처럼 app 폴더 내에 넣어주면 된다.

 

// app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.cholab.kwony.jochongmu">
 
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
     <application
         android:name=".JochongmuApp"
         android:allowBackup="true"
         android:icon="@drawable/app_icon"
         android:label="@string/app_name"
         android:supportsRtl="true"
+        android:networkSecurityConfig="@xml/network_security_config"
         android:theme="@style/AppTheme">


// app/src/main/res/xml/network_security_config.xml 

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

 

3. Ad UI 뷰 추가

 

광고를 추가하고 싶은 부분에 BannerAdView UI component를 추가한다. 아까 임포트한 라이브러리에서 구현한 클래스다. 여기서 주의할게 있는데 애드핏의 대시보드에서 설정한 크기보다 크기가 작으면 집계가 안된다고 한다. 이기적인 총무에서는 width를 꽉채우고 height 값을 320:50 의 비율에 맞춰서 조정하게 해서 광고의 크기를 맞췄다.

 

<com.kakao.adfit.ads.ba.BannerAdView
    android:id="@+id/fr_party_ad"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="320:50"
    app:layout_constraintBottom_toBottomOf="parent"/>

 

4. 광고 호출 

 

BannerAdView 까지 넣었으면 이제 실제로 광고를 호출하면 된다. fragment 또는 activity 단에서 광고를 호출하면 된다. 아래 코드를 보면 setCliendId로 대시보드에서 설정한 광고 id를 넣고 loadAd()를 호출하면 된다. setAdListener를 통해서 광고 호출 후의 작업에 리스너를 등록할 수 있는데 이건 옵션기능이다. 

 

fr_party_ad.run {
    setClientId("광고단위id")
    setAdListener(object: AdListener {
        override fun onAdLoaded() {
            JoLog.d("ad loaded")
        }

        override fun onAdFailed(p0: Int) {
            JoLog.d("failed to upload")
        }

        override fun onAdClicked() { }
    })
    loadAd()
}

광고단위는 대쉬보드에서 아래 그림의 빨간색에 있는 값이다. 알파벳과 숫자로 섞여있는 난해한 키 값처럼 생겼다.

광고단위 ID. 타인에게 노출하면 안된다.

5. 생성주기 별로 리스너 등록 

 

왠지는 모르겠으나 안드로이드 생성 주기별로 콜백을 불러줘야한다고 한다. 안그러면 안된다고 한다.

 

override fun onPause() {
    super.onPause()
    fr_main_ad?.pause()
}

override fun onResume() {
    super.onResume()
    fr_main_ad?.resume()
}

override fun onDestroy() {
    super.onDestroy()
    fr_main_ad?.destroy()
}

 

6. 완성 

 

문제 없이 끝내면 아래 그림처럼 광고를 호출하게 된다. 구현하고 승인 전까지는 카카오 비즈니스 광고가 뜨는데 승인 후에는 카카오 애드핏에서 알아서 광고를 내려준다. 몇 일 전까지는 무신사 광고만 나왔었는데 오늘은 스푼라디오 광고가 나오고 있다. 어떤 메커니즘인지 궁금해지네.

 

7. 적용후

 

수입은 얼마 안나오지만(지금까지 편의점 캔커피 만큼도 벌지 못했다) 광고를 넣으니 책임감이 더 막중해진 느낌이다. UX도 개선하고 싶고 사소한 버그들도 어서 고치고 아이콘들도 더 이쁘게 바꿔보고 싶고 ios 버전도 만들어서 다른 유저들한테도 홍보하고 싶다. 역시 돈이 걸려있다면 사람의 마음 가짐이 달라지는걸까? ㅋㅋ

728x90

2418 insertion(+), 780 deletions(-)

 

윗 문장의 두 숫자는 2년간 회사에서 소프트웨어 개발하면서 회사 깃허브에 내가 추가하고 삭제한 라인의 수다. 순수히 개발과 관련된 코드만 카운트하고자 리팩토링과 주석을 추가한 커밋, 오픈 소스의 일부 기능을 빌려온 코드, Initial Commit 같은 것은 빼고 기능 추가 또는 버그 픽스를 위한 커밋만 추려냈다. 물론 이중에서도 다른 사람의 코드가 포함될 수 있으나 역으로 다른 사람의 커밋에도 내 코드가 포함돼 있을 것이니 어느 정도 상쇄한다고 보자. 별로 큰 차이는 없을 것이다.

 

제타위키에 따르면 휴가를 고려하지 않고 한국인이 1년간 일하는 날은 249일 정도가 된다고 한다. 여기서 2를 곱하면 난 2년간 최대 458일 일했을 것이다. 이 숫자를 내가 추가하고 삭제한 커밋의 라인 수로 나누면 평균 하루 5.27 라인을 추가하고 1.70 라인을 삭제한 결과가 나온다. 추가와 삭제를 합해서 하루에 7라인을 만들고 줄인 셈이다. 너무 적지 않은가? C언어로 "Hello World"만 출력해도 엔터를 포함해서 다섯 라인은 족히 나온다. 그동안 나는 무엇을 하면서 시간을 보내고 있었던 걸까?

 

프로그래밍을 업무 별로 나누면 코드를 짜고 리뷰 하고 받는 시간은 20~30%에 지나지 않는다. 나머지 시간은 기능을 만드는데 쓴다. 단 단순히 동작하는 코드를 만드는데 그치지 않는다. 기능을 만든다는 것은 퍼포먼스를 최적화하고 추후 변경될 소지가 있는 부분들을 예상하며 보안의 위협이 될만한 부분들을 처리할 수 있는 안정적인 코드를 만드는 것이다. 한 가지 기능을 개발할 때는 예상외로 고려할 것이 많다.

 

스마트폰의 헬스케어 앱을 만드는 개발자를 예로 들어보자. 개발자는 갤럭시 S9 이용해 걸음 수를 측정하는 기능을 만들어야 한다. 걸음수를 측정할 수 있는 알고리즘은 이미 마련되어 있으니 따로 논문을 찾아볼 필요는 없다. 이것을 구현해 스마트폰의 센서로부터 값을 읽어온 후 걸음수를 카운트한 후 메인 모듈에 값을 전달하기만 하면 된다. 아마 안드로이드에 익숙한 개발자라면 한 주 안에 끝낼 수 있을 것이다. 이미 마련된 알고리즘의 의사 코드(pseudo code)를 자바로 옮기고 값에 해당하는 변수에 센서의 값을 읽도록 바꾸면 된다. 노이즈가 좀 있겠지만 이럴 때는 센서 API에서 읽어온 값을 미세하게 조정하면 된다. 2~3일 코드 짜고 하루 이틀 정도 걸어보면서 디버깅하면 충분할 것이다.

 

만약 '디바이스가 바뀌는 경우'가 있다면 어떨까? 개발자의 코드 덕분에 서비스가 번창해서 갤럭시 S9뿐만 아니라 V30에서도 서비스를 제공해야 한다고 해보자. 가장 단순한 방법은 똑같은 코드를 그대로 V30에서 쓸 수 있는 코드로 바꾸면 된다. 단 예전에 갤럭시 S9에서 미세한 오류를 조정하기 위해 넣었던 수식들은 일일이 찾아서 치환해야 한다. 땜빵 코드이기에 이 정도의 삽질은 감수해야 한다. 그런데 중국시장에까지 진출해 샤오미 폰과 하웨이 폰에서도 지원해야 한다면 매번 코드를 바꿔서 끼워 넣을 것인가? 해마다 새로운 모델이 우후죽순으로 나오는데 모두 다 이런 방식으로 바꿔야 한다면 앞이 깜깜하다.

 

눈 앞에 있는 문제만 해결한 소프트웨어는 장기적으로 개발자를 괴롭힌다.

 

이런 삽질을 미연에 방지하기 위해선 개발 초기에 디바이스의 변경 가능성을 검토했어야 했다. 개발 당시에는 갤럭시 S9이 주 타깃이지만 서비스를 제공하는 업체라는 점에서는 헬스케어 앱은 다른 모델도 지원할 가능성이 아주 농후했다. 이점을 고려한 개발자는 알고리즘 함수의 인자에 미세한 오류를 조정한 센서 값을 넣을 수 있게 해 모델에 의존하지 않는 코드를 만들었을 것이다. 매번 똑같은 코드를 만들 필요가 없었다. 디바이스가 추가되면 몇회 디버깅 해본 후 미세 조정한 센서의 값만 수식으로 만들어주면 된다.

 

회의하다보면 칠판은 어느새 그림처럼 변한다

 

OS나 클라우드 서비스처럼 플랫폼의 역할을 하는 소프트웨어는 기능을 구현하는 것도 만만치 않다. 그래서 동료들과 회의를 하며 아이디어를 도출하는 것뿐만 아니라 개발자들은 업무 중에 책과 논문을 읽어가며 공부하고 때론 관련 컨퍼런스에 참가해 다른 개발자에게 조언을 구하기도 한다. 기능 구현 방법에 대한 검토가 끝나면 여기서 파생될 수 있는 문제점들을 검토하는 시간이 된다. 현재 구현하려는 기능이 다른 팀의 모듈에 영향을 줄 수 있지는 않은지, 다른 플랫폼과 통 방법은 적절한지, 추후 외부 모듈 통신 인터페이스가 변경될 소지가 있는지 등 소프트웨어에서 나올 수 있는 모든 복합적인 요소를 고려하는 시간이다. 충분한 검토가 이뤄질수록 완성도 있는 소프트웨어가 만들어진다.

 

코드는 여러 복합적인 요소를 충분히 고려하고 만만의 준비를 마쳤을 때 작성한다. 코드 작성은 여태껏 찾아온 솔루션을 프로그래밍 언어로 옮기는 과정일 뿐이다. 작성한 코드가 버그 없고 가독성이 훌륭하며 의도한대로 동작한다면 라인의 수가 많든 적든 중요하지 않다. 더 안정적이고 더 지속가능하며 더 퍼포먼스가 좋은 소프트웨어일수록 코드 한 줄을 짜는 데는 오랜 시간이 소요된다.

728x90

'개발자의 생각 > 브런치' 카테고리의 다른 글

코드 한 줄 짜는데 시간이 오래 걸린다  (1) 2020.11.24
  1. J_jaeyoung 2020.12.31 13:37  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사합니다.

tableview

개발/iOS 2020. 11. 24. 13:37 Posted by 아는 개발자

안드로이드의 listview, recyclerview 처럼 ios에서도 여러 개의 동일한 형태의 아이템을 리스트 형태로 보여주는 UI 라이브러리가 있는데 바로 tableview 다. ios의 tableview는 크게 여러 개의 아이템을 바인딩하는 tableview와 각 table 안에 item을 그리는 cell로 이뤄져 있는데 안드로이드의 recyclerview와 viewholder 간의 관계와 동일하게 보면 될 것 같다. 이번 포스트에서는 view controller에서 가지고 있는 데이터를 tableview를 이용해서 리스트의 형태로 보여주는 간단한 예제를 다뤄보려고한다. 

 

1. cell 생성 

 

테이블에서 보여줄 아이템의 UI를 스토리보드의 형태로 그리는 작업이다. New File -> Cocoa Touch Class -> Subclass Of UITableViewCell 선택, Also create XIB file 선택 으로 아이템의 UI를 직접 그릴 수 있다. 스토리보드에서 했던것 처럼 디자인 하고, Cell의 identifier 값을 입력해둔다.

 

cell도 storyboard처럼 assitant를 사용해서 편집 할 수 있다.

 

2. tableview 초기화 

 

선작업으로 스토리보드에 추가한 TableView를 ViewController에 바인딩하고 리스트에 보여줄 아이템을 messages 변수 내에 담았다.

class ChatViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var messageTextfield: UITextField!
    
    var messages: [Message] = [
        Message(sender: "1@2.com", body: "Hey"),
        Message(sender: "a@b.com", body: "Hello!"),
        Message(sender: "1@2.com", body: "What's up")
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = K.appName
        tableView.delegate = self
        navigationItem.hidesBackButton = true
        
        tableView.register(UINib(nibName: "MessageCell", bundle: nil), forCellReuseIdentifier: "ReusableCell")
    }
}

extension ChatViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // click listener
        print(indexPath.row)
    }
}

ViewController 내에서 tableview의 delegate과 datasource를 초기화해준다. tableView의 변수 delegate는 tableview 내의 item을 클릭 할 때 이벤트를 받기 위한 콜백이고 dataSource는 ViewController에서 갖고 있는 아이템을 바인딩하기 위함이다. 

 

그 아래 tableView.register() 함수를 호출하는데 이것은 tableview에서 사용할 cell을 등록하는 작업이다. nibName으로 커스텀 디자인한 cell을 선택할 수 있는데 앞서 디자인한 파일인 "MessageCell"을 선택하고 forCellReuseIdentifier로는 앞서 정의한 "ReusableCell"을 둔다. 

 

3. data 바인딩

 

 extension ChatViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! MessageCell
        
        cell.label.text = messages[indexPath.row].body
        return cell 
    }
}

 

tableView.dataSource = self 을 구현하는 부분이며 실제로 ViewController에서 갖고 있는 메시지에 넣는 작업이다. 첫번째 함수에서는 item으로 표시할 메시지의 개수를 리턴하고 두번째 함수에서는 tableview에서 만든 cell에 데이터를 등록한다. 앞서 cell의 identifier을 ReusableCell으로 지정했으므로 withIdentifier로 동일한 값을 넣어서 받을 수 있는 메시지 셀을 받고 MessageCell로 타입캐스팅해서 클래스에 값을 입력하는 용도로 쓸 수 있다.

 

4. 구현 결과 

 

728x90

'개발 > iOS' 카테고리의 다른 글

URLSession  (0) 2020.11.30
Pod  (0) 2020.11.30
tableview  (0) 2020.11.24
codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23

codable

개발/iOS 2020. 11. 23. 15:49 Posted by 아는 개발자

codable은 swift4에서 추가된 프로토콜로 JSON 처리를 손쉽게 해준다. 예로 서버로부터 이런 json 결과물을 받으면 

 

{
  "coord": {
    "lon": -0.13,
    "lat": 51.51
  },
  "weather": [
    {
      "id": 721,
      "main": "Haze",
      "description": "haze",
      "icon": "50n"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 4.91,
    "feels_like": 1.93,
    "temp_min": 4.44,
    "temp_max": 6,
    "pressure": 1028,
    "humidity": 87
  },
  "visibility": 2800,
  "wind": {
    "speed": 2.1,
    "deg": 230
  },
  "clouds": {
    "all": 71
  },
  "dt": 1606106846,
  "sys": {
    "type": 1,
    "id": 1414,
    "country": "GB",
    "sunrise": 1606116755,
    "sunset": 1606147310
  },
  "timezone": 0,
  "id": 2643743,
  "name": "London",
  "cod": 200
}

 

Codable과 Struct을 조합해서 필요한 값들을 추출해줄 수 있다. JSON 값의 key와 데이터 타입만 일치하면 받아오는데는 문제 없다.

 

struct WeatherData: Codable {
    let name: String
    let main: Main
    let weather: [Weather]
}

struct Main: Codable {
    let temp: Double
}

struct Weather: Codable {
    let id: Int
    let description: String
}

////
func parseJSON(_ weatherData: Data) -> WeatherModel? {
    let decoder = JSONDecoder()
    
    do {
        let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
        
        let id = decodedData.weather[0].id
        let temp = decodedData.main.temp
        let name = decodedData.name
        let weather = WeatherModel(conditionId: id, cityName: name, temperature: temp)
        return weather
    } catch {
        delegate?.didFailWithError(error)
        return nil
    }
}
728x90

'개발 > iOS' 카테고리의 다른 글

Pod  (0) 2020.11.30
tableview  (0) 2020.11.24
codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
TAG Codable, ios, Swift

extension

개발/iOS 2020. 11. 23. 15:39 Posted by 아는 개발자

swift의 extension 은 클래스나 프로토콜에 새로운 함수를 추가해주는 기능을 제공한다. 일반적인 경우 클래스에 새로운 함수를 추가할 때 클래스를 수정하면 되지만 import 해서 사용중인 클래스의 경우에는 수정 할 수가 없다. 클래스는 수정 할 수 없으나 범용적으로 사용하는 함수의 경우에는 extension을 사용하면된다. 

 

예를들어 아래 코드처럼 둥근 빨간 버튼을 만드는 경우 이렇게 구현을 하면 가능하긴 하나 매번 이런식으로 만들어야되면 코드의 가독성이 떨어진다. 

 

이런 경우 extension을 이용해 UIButton 클래스에 새로운 함수, makeCircular를 만들 수 있다. 이러면 앞서 둥근 버튼을 만들기 위해 실행한 코드를 하나의 함수에 넣고 호출하는 식으로 해결 할 수 있다. UIButton 클래스를 수정 할 수는 없지만 새로운 함수를 추가함으로써 가능한 방식이다.

 

 

extension은 protocol에 적용할 때 유용한데 이때는 함수의 기본 body를 만들어줘서 상속받을 때 기본 body가 있는 함수들은 따로 구현하지 않아도 된다는 장점이 있다.  아래 코드를 보면 WeatherManagerDelegate라는 프로토콜은 두개의 함수가 있는데 그 아래 extension에서 didFailWithError 함수에 대해서 body를 넣어뒀다. 

 

protocol WeatherManagerDelegate {
    func didUpdateWeather(_ weatherManager: WeatherManager, _ weather: WeatherModel)
    
    func didFailWithError(_ error: Error)
}

extension WeatherManagerDelegate {
    func didFailWithError(_ error: Error) { }
}

 

이렇게 기본 body를 세팅해주면 WeatherManagerDelegate를 받는 쪽에서는 didUpdateWeather 함수만 구현하면 되고, didFailWithError는 구현하지 않아도 된다. 반드시 필요한 함수만 구현토록해 가독성을 좋아지게 하는 방법이다.

728x90

'개발 > iOS' 카테고리의 다른 글

tableview  (0) 2020.11.24
codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
protocol  (0) 2020.11.21

closure

개발/iOS 2020. 11. 23. 13:37 Posted by 아는 개발자

프로그래밍 함수에서는 int, float 같은 데이터 타입 인자 뿐만 아니라 함수를 전달할 수도 있는데 swift에서도 동일하게 가능하다. 함수의 인자로는 이미 있는 함수를 넣을 수 있고 뿐만 아니라 클로저(closure)의 형태로 익명의 함수(anonymous)를 만들어서 쓰는 것도 가능하다. 예로 아래에 있는 코드들은 모두 동일한 기능을 하는데 함수를 전달하는 것과 다른 클로저 형태를 사용한 것만 다르다. 자바나 코틀린을 써본 사람들은 람다 함수라는 이름으로 이미 익숙할 것이다.

 

func calculator (n1: Int, n2: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(n1, n2)
}

func multiply(no1: Int, no2: Int) -> Int {
    return no1 * no2
}

let res1 = calculator(n1: 2, n2: 3, operation: multiply)
let res2 = calculator(n1: 2, n2: 3, operation: { (no1: Int, no2: Int) -> Int in
    return no1 * no2
})
let res3 = calculator(n1: 2, n2: 3, operation: { (no1, no2) in no1 * no2 })
let res4 = calculator(n1: 2, n2: 3, operation: { $0 * $1 })

print("res1: \(res1) res2: \(res2) res3: \(res3) res4: \(res4)")

각 함수의 결과값은 6으로 모두 동일하다

클로저를 쓰면 함수를 짧고 유연하게 만들 수 있다는 장점이 있다. 하지만 $0, $1 까지 사용하면 가독성(readability)이 떨어질 수도 있기 때문에 적재적소에 사용하는 것이 좋겠다.

728x90

'개발 > iOS' 카테고리의 다른 글

codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
protocol  (0) 2020.11.21
Delegate  (0) 2020.11.21

segue

개발/iOS 2020. 11. 23. 12:24 Posted by 아는 개발자

segue(세그웨이)는 swift에선 ViewController 간의 전환하는 용도로 사용하는 라이브러리인데 ViewController가 하나의 화면을 담당하므로 화면 전환을 위한 라이브러리라고 봐도 될 것 같다. segue 가 포르투갈어로 팔로우라는 뜻을 가지고 있으니 단어의 의미와 함께 용도를 기억하면 좋을 것 같다.

 

세그웨이는 뷰 컨트롤러간의 연결을 통해서 만들수 있다. 스토리보드에서 작업중인 두개의 뷰컨트롤러중 화면전환이 시작되는 곳에서 도착지점까지 컨트롤을 눌러서 쭉 끌어준다. 실제 코드에 바인딩 해줄때 처럼 말이다. 

 

xcode는 왜이렇게 드래그를 좋아하는 걸까

 

옮기고 나면 위 그림의 파란색 박스로 표시한 새로운 아이템이 생긴다. 클릭하면 오른쪽 상단에서 세부 속성을 정의해 줄 수 있는데 identifier와 종류를 선택할 수 있다. identifier는 화면 전환 이벤트의 id와 같은 개념이고 kind는 새로운 view controller를 어떻게 띄워줄지를 선택 할 수 있다. 선호하는 방식대로 띄워주면 된다.

 

 

스토리보드에서 화면 전환에 대해서 정의한 후 ViewController에선 앞서 정의한 액션을 호출하는 역할을 한다. 아래 코드를 보면 calculatePressed 라고 스토리보드에 바인딩된 함수에서 performSegue 함수를 호출한다. 이때 withIdentifier에 들어가는 인자를 보면 아까 스토리보드에서 정의한 identifier와 동일한 값이 들어가있는걸 볼 수 있다. identifier 값을 확인해서 어떤 세그웨이를 사용할 지 선택하는 것이다. 

 

    @IBAction func calculatePressed(_ sender: UIButton) {
        print(heightSlider.value)
        print(weightSlider.value)
        
        let height = heightSlider.value
        let weight = weightSlider.value
        
        calculatorBrain.updateBmi(height, weight)

        self.performSegue(withIdentifier: "goToResult", sender: self)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "goToResult" {
            let destinationVC = segue.destination as! ResultViewController
            
            destinationVC.bmiValue = calculatorBrain.bmi
        }
    }

 

performSegue 함수를 호출하면 아래 오버라이딩된 prepare 함수가 호출되는데 이때 어떤 세그웨이가 호출됐는지 인자를 확인 할 수 있고 새로 생성된 ViewController 함수를 볼 수 있다. 위 코드를 보면 세그웨이의 도착점을 ResultViewController 클래스로 타입 캐스팅 한 것을 볼 수 있다. 그리고 새로운 ViewController에 전달하고 싶은 bmiValue를 입력하고 있는 것도 볼 수 있다.

728x90

'개발 > iOS' 카테고리의 다른 글

codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
protocol  (0) 2020.11.21
Delegate  (0) 2020.11.21

protocol

개발/iOS 2020. 11. 21. 16:19 Posted by 아는 개발자

protocol은 네트워크나 의전에서 주로 쓰이는 용어인데 swift에서는 네트워크와 의전에서 사용되는 의미랑은 조금 다른 차원인 것 같고 차라리 자바나 코틀린에서 사용하는 interface가 좀더 와닿는 느낌이며 실제로도 비슷한 역할을 한다(자바 개발자를 너무 오래해서 그런가). 이 포스트에서는 객체 지향적 관점에서 protocol을 왜 사용해야하는지를 설명하기 보다는 어떻게 쓰는지를 중점적으로 설명하고자 한다.

 

protocol은 delegate 패턴을 쓸 때 사용하는 타입이다. delegate 패턴을 사용하는 예제 코드를 통해 protocol을 살펴보자.

 

protocol AdvancedLifeSupport {
    func performCPR()
}

class EmergencyCallHandler {
    var delegate: AdvancedLifeSupport?
    
    func assessSituation() {
        print("Can you tell me what happened?")
    }
    
    func medicalEmergency() {
        delegate?.performCPR()
    }
}

 

EmegencyCallHandler는 위급 상황을 평가하는 함수(assessSituation) 와 처리하는 함수(medicalEmergency)를 갖고 있는데 처리하는 작업은 다른 클래스에서 작업을 위임하고 있다. 위임할 때 호출하는 변수인 delegate는 AdvancedLifeSupport 라는 프로토콜 타입의 클래스로 선언돼있다. 

 

AdvanceLifeSupport protocol을 보면 performCPR() 함수만 있고 내부의 body는 구현되지 않았다. 이 함수는 이 명령을 위임 받을 클래스에서 반드시 구현해야 하는 내용이다.

 

class Doctor: AdvancedLifeSupport {
    func performCPR() {
        print("The doctor does chest compressions, 30 per second.")
    }
    
    init (handler: EmergencyCallHandler) {
        handler.delegate = self
    }
}

let handler = EmergencyCallHandler()
let doctor = Doctor(handler: handler)

handler.assessSituation()
handler.medicalEmergency()

 

Doctor 클래스에서는 AdvancedLifeSupport protocol을 구현하고 있다. body만 빠져 있던 performCPR 함수에 Doctor 클래스에서 처리하고 있다는 내용이 담긴 로그를 남기고 있다. 이 함수를 구현하지 않으면 컴파일 에러가 뜬다. 이 함수를 위임 받았으니, 해당하는 작업을 처리하라는 뜻이다.

 

아래 클래스에서 EmergencyHandler와 Doctor 클래스를 각각 초기화하고 Handler 에서 함수를 호출하면 다음과 같은 콘솔이 뜨는 것을 볼 수 있다. EmergencyHandler에서 작업을 위임 받은 Doctor 클래스에서 선언한 performCPR 함수가 호출되고 있다.

 

 

728x90

'개발 > iOS' 카테고리의 다른 글

codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
protocol  (0) 2020.11.21
Delegate  (0) 2020.11.21
TAG ios, Protocol

Delegate

개발/iOS 2020. 11. 21. 16:02 Posted by 아는 개발자

swift 언어에서 사용하는 delegate 패턴은 용어에도 담긴 의미 처럼 특정한 작업을 다른 클래스에 위임 할 때 사용하는 디자인 패턴이다. 프로그래밍을 처음 경험하는 사람들은 어렵게 느낄 수 있으나 다른 언어를 먼저 경험해본 사람들한테는 콜백과 비슷한 사용 용도라고 될 것 같다. UI 클래스에서 주로 사용하는데 UITextField 클래스에서 사용 예시를 한번 확인해보자.

 

 

위 그림의 상단에 사용자로부터 텍스트를 받을 수 있도록 UITextField를 만들어뒀다. 시뮬레이터에서 유저가 이 영역을 클릭하면 자동으로 키보드가 올라오게 되는데 UITextField만 추가하고 아무런 추가 작업이 없었다면 엔터에 해당하는 이동 버튼을 눌러도 아무런 응답이 없다. 엔터 명령에 대해서 어떤 클래스도 이 작업을 위임 받지 않았기 때문이다.

 

iOS 상에선 특정 TextField 내에서 엔터 명령은 UITextField 내부적으로 처리하고 있다. 유저가 엔터를 여러번 클릭할 수록 UITextField 내부에서는 엔터 명령을 받게 된다. 개발자가 할 일은 UITextField의 엔터 작업을 위임 받을 클래스를 선언하고 그에 해당하는 명령을 처리하는 것이다. 다양한 방법으로 이 작업을 할 수 있는데 일반적으로 UITextField가 있는 ViewController에서 이 작업을 위임해준다

 

class WeatherViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var conditionImageView: UIImageView!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var searchTextField: UITextField!
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        searchTextField.endEditing(true)
        print(searchTextField.text!)
        
        return true
    }    

 

위의 코드를 보면 ViewController 클래스에서 UITextFieldDelegate 프로토콜을 받고 있고 이 프로토콜에 있는 textFieldShouldReturn 함수를 구현해둔 것을 볼 수 있다. 이 함수는 해당 UITextField의 키보드에서 엔터 명령이 있는 경우에 불린다. 위임 받은 클래스에선 편집 작업을 종료하고 현재 입력된 값을 출력하는 작업을 넣었다.

 

public protocol UITextFieldDelegate : NSObjectProtocol {

    
    @available(iOS 2.0, *)
    optional func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool

    @available(iOS 2.0, *)
    optional func textFieldDidBeginEditing(_ textField: UITextField)

    @available(iOS 2.0, *)
    optional func textFieldShouldEndEditing(_ textField: UITextField) -> Bool

    @available(iOS 2.0, *)
    optional func textFieldDidEndEditing(_ textField: UITextField)

 

UITextField에선 다른 작업에 대해서도 위임 받을 수 있는데 입력을 시작할 때 호출되는 textFieldDidBeginEditing 과 입력이 끝날 때 호출되는 textFieldDidEndEditing 같은 것들이 있다. 실제 코드로 따라가보면 더 많은 함수들이 있으니 구현할 때 참고해보면 좋을 것 같다.

 

이렇게 작업을 위임하는 delegate 패턴은 protocol 이라는 것을 사용해서 구현한다. 자바의 interface와 엇비슷하고 코틀린의 interface와는 아주 비슷한데 이에 대한 내용은 다음 포스트에 다루려고 한다.

728x90

'개발 > iOS' 카테고리의 다른 글

codable  (0) 2020.11.23
extension  (0) 2020.11.23
closure  (0) 2020.11.23
segue  (0) 2020.11.23
protocol  (0) 2020.11.21
Delegate  (0) 2020.11.21

이전 포스트에서는 카메라에서 담고 있는 프레임을 OpenGL로 그린 후 GLSurfaceView로 그려주는 작업을 했었다. 지금부터는 그려진 이미지를 비디오 파일로 만드는 작업에 대해서 분석해보고자 한다.

 

3. 미리보기 영상 인코딩하기 

 

MediaCodec을 사용한 비디오 인코딩 작업도 Renderer와 동일하게 OpenGL을 이용한 그리기 작업이 필요하다. 전반적인 구현 아이디어는 비디오 녹화용 EGL Context를 선언한 후 Renderer 클래스로부터 현재 촬영 중인 카메라의 이미지를 받아와 OpenGL로 다시 그려주고 Media Codec에서 받을 수 있는 Surface 형태로 보내는 것이다.

 

3.1 비디오 인코딩용 EGL Context 선언 

 

비디오 녹화 작업도 OpenGL 작업이 필요하므로 OpenGL 작업용 EGLContext를 만들어준다. 이때 Renderer로부터 카메라 촬영 이미지를 받아오기 위해 EGL 초기화 작업에 공유 EGL Context 정보(shared_context)를 추가한다. 

fun setVideoEncoder(videoEncoder: MediaVideoEncoder?) {
    this.videoEncoder = videoEncoder

    videoEncoder?.setEglContext(EGL14.eglGetCurrentContext(), textureId)
}

private fun createContext(shared_context: EGLContext?): EGLContext {
    val attrib_list = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
    val context =
        EGL14.eglCreateContext(mEglDisplay, mEglConfig, shared_context, attrib_list, 0)
    checkEglError("eglCreateContext")
    return context
}

 

3.2 Renderer로부터 카메라 이미지 받아오기 

 

Renderer로부터 새로운 프레임이 발생했다는 콜백을 받으면 VideoEncoder는 카메라로부터 이미지를 받아와서 새롭게 그려주게 된다. 카메라 이미지는 Renderer 클래스 내의 texture에 있으며 고유한 texture id를 EGL내에서 bind 해서 받아 올 수 있게 된다. VideoEncoder 클래스에 해당 textureId를 전달해서 VideoEncoder의 EGLDisplay에 그려준다.

fun draw(tex_id: Int, tex_matrix: FloatArray?) {
    GLES20.glUseProgram(hProgram)
    if (tex_matrix != null) GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, tex_matrix, 0)
    GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mMvpMatrix, 0)
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex_id)
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_NUM)
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
    GLES20.glUseProgram(0)
}

 

3.3 MediaCodec으로 이미지 버퍼 전달 

 

인코딩을 위해 새롭게 그린 이미지를 MediaCodec에서 만든 Surface에 버퍼로 전달한다. OpenGL 에서 제공하는 eglSwapBuffers 함수를 사용하면 MediaCodec이 받을 수 있는 surface에 전달이 가능하다.

private fun swap(surface: EGLSurface?): Int {
    if (!EGL14.eglSwapBuffers(mEglDisplay, surface)) {
        val err = EGL14.eglGetError()
        if (DEBUG) Log.w(TAG, "swap:err=$err")
        return err
    }
    return EGL14.EGL_SUCCESS
}

 

3.4 전달 받은 정보를 인코딩 

 

MediaCodec 고유 함수를 이용해 전달받은 정보를 인코딩한다. MediaCodec 관련 코드는 생략한다. 

 

4. 마치며 

 

포스트에선 전반적인 구현 아이디어만 다루었기 때문에 보기에는 쉽지만 실제 사용된 코드는 꽤 복잡했다. google에서 짜둔 클래스 간의 상속과 인터페이스 관계를 따라가는 게 생각보다 시간이 걸렸고 아직도 생소한 OpenGL 클래스의 역할과 내부 코드를 알지 못해 문서를 찾아가느라 어려웠다.

 

그래도 고생하면서 생소했던 카메라와 OpenGL 관련 지식을 배운게 개발자로서 큰 소득이다. 스노나 틱톡의 카메라 효과 코드를 보진 못했지만 아마 위 구현 방식과 크게 차이가 나지 않을 것 같다. 여기에 OpenGL 코드를 더 확장시키면 나도 촬영 중인 화면에 여기에 필터를 변경하고 스티커도 추가해볼 수 있겠다. 현재 구현된 코드를 한층 더 업그레이드시켜봐야겠다.

 

오디오까지 같이 녹화하고 싶다면 여기 깃허브 코드를 참조하면 좋다.

 

https://github.com/saki4510t/AudioVideoRecordingSample

728x90

0. MediaRecorder의 한계

 

구글이 운영 중인 안드로이드 카메라 Sample 코드 저장소에선 Camera2Camera 2 라이브러리를 이용해 사진을 찍거나 비디오 녹화를 할 수 있는 예제가 있다. Camera 2 Video 프로젝트의 비디오 녹화 예제 코드의 경우 카메라에서 출력되는 프레임을 MediaRecorder라는 클래스를 이용해서 녹화할 수 있도록 했는데 이 방식은 후면 녹화의 경우에는 별로 문제가 없으나 전면 카메라를 이용하는 경우 미리보기에서 나온 영상이 그대로 저장되지 않고 좌우가 반전돼서 나오게 되는 문제가 있다. 대부분 카메라 어플에서 제공하는 옵션인 보이는 대로 저장 하기 기능을 사용할 수 없는 큰 문제점(?) 이 존재한다

 

보이는대로 저장 옵션을 사용할 수 없다. 그래서 촬영한후 저장한 내 모습이 아주 어색하게 저장된다

 

전면 카메라에 출력된 내 모습 그대로 저장하기 위해선 MediaRecorder 클래스 대신 대신 카메라에서 출력된 프레임을 OpenGL 그래픽 라이브러리를 이용해 렌더링 한 후 화면에 출력된 프레임을 MediaCodec을 이용해 직접 비디오 파일을 만드는 과정이 필요하다. MediaRecorder를 사용하는 코드가 워낙 간편했거니와 그래픽 라이브러리와 MediaCodec을 사용하는 작업은 대부분 개발자들에게도 생소한 OpenGL 지식이 필요하기 때문에 다소 까다롭다. 하지만 이것 말고는 전면 카메라를 반전시킬 방법은 없기 때문에 어렵더라도 직접 구현해봤다.

 

1. 오픈소스

 

다행히 구글의 비공식 저장소인 grafika에서 이미 구현한 코드가 있었다. 카메라에서 촬영중인 프레임을 안드로이드 그래픽 라이브러리에 렌더링 한 후 화면에 출력된 이미지를 MediaCodec을 이용해 MP4의 파일로 만드는, 앞서 의도한 방식을 그대로 구현한 코드였다. 그런데 3-4년 전에 작성한 코드라서 현재는 Deprecated 된 Camera 라이브러리를 사용 중이어서(현재는 Camera 2를 주로 쓰고 CameraX 알파 버전이 개발 중이다) grafika 코드를 분석하고 여기서 동작하는 모듈을 Camera 2랑도 연동이 될 수 있도록 하는 방향으로 개발했다.

 

2. Camera2와의 GLSurfaceView 연동 과정 

 

camera2 구조

grafika에서 이미 구현한 부분은 Surface, Renderer, GLSurface 간의 연동 과정이고 내가 추가적으로 넣은 부분은 Camera 2와 Renderer에서 만든 Surface를 연동한 부분뿐이다. 연동 과정과 각 클래스의 역할을 분석한 내용을 단계별로 정리했다.

 

2.1 Renderer 초기화 작업

 

GLSurfaceView는 OpenGL로 그려진 이미지를 안드로이드 UI에 노출 시켜줄 수 있는 클래스다. Renderer 클래스는 GLSurfaceView에 표시할 이미지를 OpenGL로 그리는 역할을 한다. Renderer 클래스가 GLSurfaceView의 그리는 역할을 담당할 수 있도록 setRenderer  함수를 이용해 두 클래스를 연결시켜준다. 이러면 Renderer에서 그린 OpenGL 이미지가 GLSurfaceView에 표시된다.

 

class CameraSurfaceRenderer(private val glSurfaceView: GLSurfaceView) : GLSurfaceView.Renderer,

    init {
        Matrix.setIdentityM(mvpMatrix, 0)
        glSurfaceView.setEGLContextClientVersion(2)
        glSurfaceView.setRenderer(this)
        glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
    }

 

연결 작업후 OpenGL을 이용해 그릴 수 있는 공간을 선언하는 초기 작업이 필요한데 이 작업은 Surface가 생성된 이후에 불리는 onSurfaceCreated 콜백 함수에서 담당한다. 이 함수 내에서는 OpenGL Texture를 생성하는 초기화 외부로부터 이미지 스트림을 받을 SurfaceTexture를 선언한다. SurfaceTexture는 OpenGL Texture로 이미지 스트림을 보내는 역할을 한다.

 

class CameraSurfaceRenderer(private val glSurfaceView: GLSurfaceView): GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
    @SuppressLint("Recycle")
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        hTex = GLDrawer2D.initTex()
        surfaceTexture = SurfaceTexture(hTex)
        surfaceTexture?.setOnFrameAvailableListener(this)

        // clear screen with yellow color so that you can see rendering rectangle
        GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f)

        drawer = GLDrawer2D()
        drawer.setMatrix(mvpMatrix, 0)
    }
}

 

2.2 Camera2 촬영 중인 공간 표시

 

Camera 2에서는 Surface 클래스의 형태로 카메라에서 보고 있는 이미지를 전달받을 수 있다. 앞서 Renderer에서 외부로부터 이미지를 받을 공간을 SurfaceTexture로 선언했는데 카메라의 이 클래스를 이용해 Surface를 만들면 카메라로부터 이미지를 전달받고 미리보기로 보여줄 수 있게 된다.

private fun startRecordingVideo() {
    if (cameraDevice == null) return

    try {
        closePreviewSession()

        // Set up Surface for camera preview
        val previewSurface = Surface(renderer.surfaceTexture)
        val surfaces = ArrayList<Surface>().apply {
            add(previewSurface)
        }
        previewRequestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
            addTarget(previewSurface)
        }

 

2.3. 카메라로 부터 받은 이미지 스트림을 OpenGL로 그리기

 

카메라에서 Renderer에서 선언한 SurfaceTexture에 이미지 스트림으로 보내주는 것이 됐으니 카메라가 보고 있는 이미지 스트림 정보를 실제로 OpenGL 코드로 그려주는 작업이 필요하다. Renderer 함수에는 두 개의 콜백 함수가 있는데 카메라에서 이미지를 전달받은 SurfaceTexture가 호출하는 onFrameAvailable() 콜백이 먼저 불린다. 이 함수에선 최신 이미지가 도착했으니 현재 Renderer와 연동된 GLSurfaceView에게 업데이트를 요청하는 함수인 requestRender() 함수를 호출한다.

 

requestRender() 호출 후엔 연달아서 onDrawFrame() 이 불리는데 여기선 SurfaceTexture로 전달받은 카메라의 프레임 정보를 OpenGL Texture에 업데이트 시킨 후 OpenGL 명령어로 화면에 그리는 작업을 한다. 이 작업이 생략되면 카메라 초기화 작업은 잘 됐음에도 불구하고 화면에는 검은 화면만 뜨게 된다. OpenGL로 그림을 그리는 코드는 생략했다.

 

override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    requesrUpdateTex = true
    glSurfaceView.requestRender()
}

override fun onDrawFrame(gl: GL10?) {
    if (requesrUpdateTex) {
        requesrUpdateTex = false
        surfaceTexture?.updateTexImage() // 전달 받은 카메라 이미지 프레임을 OpenGL Texture 에 업데이트
        surfaceTexture?.getTransformMatrix(stMatrix)
    }

    drawer.draw(hTex, stMatrix) // Texture 가지고 OpenGL로 그림을 그린다
    if (recording) {
        videoEncoder?.frameAvailableSoon(stMatrix, mvpMatrix)
    }
}

 

여기까지 완료하면 Camera2를 이용해서 출력 중인 화면을 OpenGL 코드로 화면에 보여주는 것까지 가능하다. 그러나 녹화를 하기 위해선 Renderer에서 출력되고 있는 프레임 정보를 MediaCodec으로 보내서 MP4 파일을 만드는 작업까지 가야 하는데 이 내용은 다음 포스트에서 다룰 예정이다.

728x90