마지막 예제 분석으로 DIH (Data Import Handler)를 정리한다.

앞서의 기술제품과 영화 예제들은 정리된 데이터를 저장(색인)한 것이고,

DIH는 원본 데이터에서 데이터를 추출해서 저장하는 방법에 대한 예제이다.

  1. 설치
  2. 기술제품과 검색식
  3. 스키마(Schema)
  4. DIH (Data Import Handler)

 

Solr 예제 폴더(example\example-DIH\)에는 atom, db, mail 등의 데이터 예제가 있다.

이 중에서 db (hsqldb) 예제를 정리한다.

주의: SQL이나 데이터 베이스에 대한 개념이 없다면, 다음 내용을 이해하는데 어려울 수 있다.


실행 중인 Solr를 멈추고,

      bin\solr stop -all

다음 명령어로 DIH 예제를 실행한다.

      bin\solr -e dih

Solr 관리자 화면으로 접속하면 5개의 코어가 생성된 것을 확인할 수 있다. 

이 중에서 db 코어를 선택하고, Query 화면에서 데이터를 조회하면 비어 있다 (numFound=0).

Solr 관리자 화면의 Dataimport 메뉴를 선택한 후, 화면 오른쪽의 Configuration을 클릭해서 실행할 SQL문을 확인한다.

중앙의 실행(Execute) 버튼을 클릭해서, 이 SQL문을 실행한다.

실행(Execute) 버튼을 클릭하고, Auto-Refresh Status를 체크해서 자동으로 갱신되게 하거나,

Refresh Status를 클릭해서 수동으로 갱신해서 처리 결과를 확인한다.

녹색 배경으로 16개의 데이터(document)가 저장되었다는 메시지가 나타나면, 잘 실행된 것이다.

Query 메뉴에서 저장된 데이터를 확인 할 수 있다.

앞서 정리한 기술 제품의 내용과 동일한 데이터로,

기술 제품 예제는 이 데이터 베이스의 내용을 XML로 만든 파일을 저장한 것이다.

 

간단하게 db 예제 사용법을 정리했고, 상세한 내용을 정리한다.

예제로 사용된 db는 hsqldb이지만

Dataimport 메뉴에서 확인한 다음의 SQL문을 보면 기본적인 SQL문만 사용되었기 때문에 hsqldb라는 것에 부담을 가질 필요는 없다.

<dataConfig>
    <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:${solr.install.dir}/example/example-DIH/hsqldb/ex" user="sa" />
    <document>
        <entity name="item" query="select * from item"
                deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'">
            <field column="NAME" name="name" />

            <entity name="feature"  
                    query="select DESCRIPTION from FEATURE where ITEM_ID='${item.ID}'"
                    deltaQuery="select ITEM_ID from FEATURE where last_modified > '${dataimporter.last_index_time}'"
                    parentDeltaQuery="select ID from item where ID=${feature.ITEM_ID}">
                <field name="features" column="DESCRIPTION" />
            </entity>
            
            <entity name="item_category"
                    query="select CATEGORY_ID from item_category where ITEM_ID='${item.ID}'"
                    deltaQuery="select ITEM_ID, CATEGORY_ID from item_category where last_modified > '${dataimporter.last_index_time}'"
                    parentDeltaQuery="select ID from item where ID=${item_category.ITEM_ID}">
                <entity name="category"
                        query="select DESCRIPTION from category where ID = '${item_category.CATEGORY_ID}'"
                        deltaQuery="select ID from category where last_modified > '${dataimporter.last_index_time}'"
                        parentDeltaQuery="select ITEM_ID, CATEGORY_ID from item_category where CATEGORY_ID=${category.ID}">
                    <field column="DESCRIPTION" name="cat" />
                </entity>
            </entity>
        </entity>
    </document>
</dataConfig>

SQL이 나열된 XML은 Dataimport 메뉴에서 확인 할 수도 있고,

db-data-config.xml 설정(example\example-DIH\solr\db\conf)파일에서도 확인/ 수정 할 수 있다.

참고: Solr의 기본 설정 파일인 solrconfig.xml에서 다음과 같이 db-data-config.xml 을 지정해야 db-data-config.xml 에서 위와 같은 내용을 작성해서 사용할 수 있다.

 <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-\d.*\.jar" />

생략

  <requestHandler name="/dataimport" class="solr.DataImportHandler">
    <lst name="defaults">
      <str name="config">db-data-config.xml</str>
    </lst>
  </requestHandler>

 

dataSource는 Java의 dataSource와 같은 것으로, 데이터베이스 접속에 필요한 driver, url과 id, password등을 입력한다.

예제에서는 sa 계정에 비밀번호없이 접속한다.

 

이상의 SQL문을 하나 하나 정리할 수는 없고, 개념으로 정리한다.

사용된 hsqldb의 테이블 구조를 알아야 사용된 SQL문을 이해하기 쉽다.

hsqldb의 테이블 구조와 데이터는 example\example-DIH\hsqldb 폴더에 있는 ex.script 파일에 정의되어 있다.

이 내용 중 테이블 정의와 관련된 내용을 ERD로 정리하면 다음 그림과 같다.

ITEM(제품) 테이블이 기술 제품의 핵심 테이블로, 제품의 특성(FEATURE) 및 종류(CATEGORY) 테이블과 1:n으로 연결된다.

제품 테이블의 ID가 기본키(Primary Key)로, 특성(FEATURE)과 종류(ITEM_CATEGORY) 테이블의 ITEM_ID 필드(Foregin Key)와 관계(Relation)를 가진다.

즉, 하나의 제품이 여러 개의 특성과 종류를 가진다.

ITEM_CATEGORY은 연관 테이블(Association Table)로 여러개의 종류를 가지기 위해 사용되었다

(데이터 베이스와 관계된 개념은 여기서는 정리하지 않는다.)

주의: 데이터베이스에서는 이와 같이 데이터를 정규화해서 저장하지만, 검색엔진에서는 하나의 데이터로 묶어서 저장한다.

 즉, 데이터베이스에서 1:n으로 구성한 데이터는 검색엔진에서 배열([ ]), 즉 multiValued로 저장한다.

 

dataSource 태그는 접속 정보를 입력하고,

document 태그는 처리할 데이터를 정의하는 태그로 기본 구조는 다음과 같다.

    <document>
        <entity name="item" query="select * from item"
                deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'">
            <field column="NAME" name="name" />
        </entity>
    </document>		

문서(document)를 구성하는 것을 entity라고 한다.

이 entity는 처음에는 query에 지정된 데이터로 채우고, 이후에 증가된 데이터는 deltaQuery로 지정된 데이터로 채운다.

처음에는 기존에 있던 데이터에 대해서(query) 전체 색인을 진행하고(Full indexing), 이후에는 증가된 데이터만(deltaQuery) 색인하도록 작성한다.

즉, 처음에는 모든 제품을 가져오고 select * from item

마지막 색인을 한 시간(last_index_time) 이후에 추가된 제품(last_modified)만 조회해서 색인한다.

last_modified > '${dataimporter.last_index_time}'

 

이렇게 문서를 생성해서 저장(색인)하면 되지만,

앞서 ERD로 정리한 것처럼 기술 제품은 여러 개의 제품의 특성(FEATURE)과 종류(CATEGORY)를 가지고 있다.

이것을 그냥 Join으로 작성하면, 1:n의 관계를 가지기 때문에 하나의 제품이 여러 개 조회되는 문제가 생긴다.

 

제품 특성(FEATURE) 테이블을 예로 정리하면,

제품(item)을 가져오는 entity 태그 하위에(안에), 특성(feature)를 가져오는 entity 태그를 추가한다.

<entity name="item" query="select * from item"
    ~~ 생략 ~~
	<entity name="feature"  
			query="select DESCRIPTION from FEATURE where ITEM_ID='${item.ID}'"
			deltaQuery="select ITEM_ID from FEATURE where last_modified > '${dataimporter.last_index_time}'"
			parentDeltaQuery="select ID from item where ID=${feature.ITEM_ID}">
		<field name="features" column="DESCRIPTION" />
	</entity>
</entity>

ERD의 관계(Relation)를 태그의 하위 계층으로 표현하는 것이다.

 

특성(FEATURE) 테이블에서 데이터를 가지고 오는 것은 제품(ITEM) 테이블에서 가지고 오는 것과 동일하다.

처음에는(query) 테이블의 모든 데이터를 가지고 오고,

이후에는(deltaQuery) 증가된 데이터만 가지고 온다. last_modified > '${dataimporter.last_index_time} 

 

여기에 추가적인 것이 parentDeltaQuery로 부모가 누구인지 지정하는 부분이 있다.

가지고 온 데이터를 누구에게 넣어줄 것인지를 지정하는 것으로,

제품(ITEM) 테이블에서 ID 필드가 기본키(paimary key)로 특성(FEATURE)테이블의 ITEM_ID 필드로 관계가 설정되어 있어, 이 값으로 서로를 식별하게 된다.

        select ID from item where ID=${feature.ITEM_ID}

즉, 특성 테이블의 값을 가지고 온 후(feature), 제품 테이블의 해당하는 ID를 찾아서,

특성의 값(DESCRIPTION)을 features로 저장(색인)한다.

        <field name="features" column="DESCRIPTION" />

주의: item(최상위 entity)에서도 field 태그를 이용해서 NAME을 name으로 저장하도록 지정했다.

색인으로 저장할 컬럼들은 모두 field 태그로 지정해야 하고, managed-schema에 정의해야 한다.

DIH에서는 managed-schema에 지정되지 않은 컬럼은 저장되지 않는다.

앞서 정리한 Shemaless가 적용되지 않는 것 같다.

 

parentDeltaQuery로 표현된 방식은 중요한 개념이라 다시 정리하면,

일반적인 S/W 개발에서는 제품(ITEM) 테이블에서 데이터를 가지고 오고, 

각각의 행에 있는 제품의 특성(ID) 필드의 값에 맞는 것을 특성(FEATURE) 테이블에서 찾아서

해당하는 설명(DESCRIPTION)을 가지고 오게 작성한다. 

그리고 SQL로 표현하면 다음과 같다.

      select a.*, 
            (select DESCRIPTION from FEATURE where ITEM_ID=a.ID) as features
      from ITEM a

주의: 제품(ITEM)과 특성(FEATURE)의 관계는 1:n 이기 때문에 이 SQL문을 그대로 쓰면 오류가 발생해서 실제론 stragg 처리를 해야 한다.

일반적인 S/W 개발에서는 제품(ITEM) 데이터를 가지고 오면서, 필요한 구성원 데이터(특성-FEATURE)을 가지고 오도록 작성한다.

(모 상용 검색엔진이 이런식으로 사용한다.)

 

이 개념과 반대로, 예제에서는 제품(ITEM) 데이터를 가지고 와서 저장하고

필요한 구성원 데이터(FEATURE)를 가지고 와서 저장된 제품(ITEM)에 넣어주는 방식으로 작성되었다.

 

이렇게 하는 이유는 제품(ITEM)과 특성(FEATURE) 테이블로 설명하는 것은 부족한 것 같아서 게시판 예제로 정리한다.

게시판에 게시물이 있고(post), 하나의 게시물에 여러 개의 댓글(reply)이 달린다.

일반적인 S/W 개발 방식으로 데이터를 추출하면, 게시물이 색인 될 때의 댓글도 같이 색인하면 된다.

하지만, 색인 된 이후에 추가된 댓글들은 색인하기 어렵다.

따라서, 게시물은 게시물 데로 색인하고, 댓글은 댓글데로 색인해서

추가 댓글이 있으면 추출해서 해당 게시물에 넣어주는 것(parentDeltaQuery)이 더 좋은 방식일 것이다.


종류(category, item_category)도 동일한데, 연관 테이블(Association Table)이 있어서 좀 더 복잡하다.

원리는 동일하니 직접 확인하고, 여기에서는 정리 하지 않는다.

 

이상의 정리는 Solr DIH 예제를 정리한 것으로, 보다 상세한 내용은 Solr DIH 예제에 정리된 내용을 읽어보면 된다.

이상으로 기술 제품에 대해 검색하는 방법,

기술 제품과 영화 정보 구축을 위해 스카마를 구성하는 방법,

스키마에 맞춰서 데이터를 저장하는 방법(색인)을 Solr 예제로 정리하였다.

 

Solr 예제 문서에 빠진 내용은 색인을 진행하는 시간 처리로,

초기에 전체 데이터를 대상으로 진행하는 풀색인은 앞서 정리한 것처럼 Dataimport 메뉴에서 실행해도 되고, 다음과 같이 RESTful로 실행해도 된다.

       curl http://localhost:8983/solr/dih/dataimport?command=full-import

풀색인은 사람이 진행하지만,
증가되는 데이터는 실시간으로 색인하는 경우가 아니면, 지정된 시간에 증가된 데이터에 대해서만 진행한다(delta-import).

지정된 시간에 실행하기 위해 별도의 프로그램을 제작하거나 플러그인을(DataImportScheduler) 이용할 수 있는데,

Solr 문서에서는 운영체제의 스케쥴 기능을 이용하길 원하는 것 같다.

좋은 스케쥴 기능이 많다며, DataImportScheduler 같은 것을 사용하는 것은 바퀴를 다시 발명하는 것이라고 한다.

증분만 실행하는(delta-import) 다음 명령어를 crontab에 등록해서 지정된 시간에 실행되게 사용한다. 

        curl http://localhost:8983/solr/dih/dataimport?command=delta-import

 

이상의 내용 외에 사용자가 입력한 검색어를 Solr로 전송하고, 결과를 받아서 출력하는 프로그램(Java)에서 사용하는 SolrJ와

색인하는 방법에 대한 구체적인 내용은 정리하지 않았다.

SolrJ와 관련된 내용은 이전 블로그 내용을 참고하거나 다른 자료를 찾아보면 된다.

색인하는 방법은 한국어 등을 색인 하기 위해서 필요한 것으로 형태소 분석과 관련된 내용을 찾아보면 된다.

 

Playground (우리동네, 아이랑 놀곳 Ver.서울)는 

서울 시내에서 아이랑 갈만한 실내외 놀이터, 박물관/미술관, 도서관등의 장소(약 3,700군데)들을 모아서, 

일목요연하게 보여 주는 웹 사이트로 

주말 같은 여유 시간에 아이랑 어디를 가야 할지 고민하는 부모들을 위해 제작한 웹 사이트이고, 

Playground 안드로이드(Android) 앱은 이 사이트의 앱 버전으로 

아직 개발 중이지만, 

다음과 같이 2 가지 핵심기능을 동일하게 제공하고 있다.

  • 우리 동네: 지도상에 보고싶은 지역의 좌표(위도, 경도)를 지정하면, 좌표를 중심으로 2Km 내의 시설/장소 정보 표시
  • 추천 코스: 특정 장소를 중심으로 갈만한 장소들을 묶어서 하나의 코스로 정리

반응형으로 제작된 웹 사이트와 앱의 핵심 기능은 동일하지만

지정된 위치의 시설들을 조회하는 [우리 동네]의 경우,

웹 사이트에는 SSL 문제 때문에 위치를 검색해서 지정하는 방식으로 구현하였다.

안드로이드 앱 버전은 GPS를 이용해서, 현재 위치를 지정하는 방식으로 구현하였다.

Playground 에 대한 웹 사이트와 기능에 대한 상세한 설명은 여기서 확인할 수 있고,

안드로이드 앱 버전의 프로그램 소스는 Github에서,

안드로이드 앱은 구글 play에서 다운 받아서 설치 할 수 있다.

 

주요 기능에 대한 사용법을 정리하면,

메뉴나 메인 화면에서 [우리동네] 를 클릭하면

다음과 같이 현재 위치나 원하는 위치를 지정하는 화면이 나타난다.

GPS로 현재 위치를 지정하거나 지도를 클릭해서 위치를 지정하고,

화면 우측 하단에 있는 조회(돋보기) 버튼을 클릭하면

지정된 위치를 중심으로 2Km내의 실내외 놀이터, 박물관등이

그림과 같이 색깔별로 표시 된다.

표시된 점(위치)를 선택하면,

해당 장소에 대한 간단한 정보가 하단에 표시된다.

 

[추천코스]도 메뉴나 메인 화면에서 클릭해서 실행할 수 있다.

서울 시내에 갈만한 곳을 묶어서 보여 주는 기능으로

현재는 관리자(개발자)가 개인 경험을 기반으로 작성한 내용들이 나타난다.

사용자가 등록하는 기능은 추가할 예정이다.

 

리스트에서 항목을 선택하면

코스에 해당하는 코스, 소개글, 지도 등의 상세한 내용을 확인할 수 있다.

안드로이드 앱은 웹 사이트에 비해

아직 세부 기능이 부족하지만, 계속 추가 개발되고 있다.

 

키보드만 사용하던 어느 날,

마우스란 것이 세상에 나타났고, 

컴퓨터로 그림 그릴 때 사용하는 것인 줄 알았다.

개발을 할 때에는 빠른 속도로 타이핑을 하고, 마지막에 엔터키를 탁 치는 맛이 있었다.
(지금도 그런 개발자를 간혹 본다)

 

군대를 제대하고 복학하니,

대부분의 사람들이 윈도우 3.0과 함께 마우스를 사용하고 있었고,

학교 실습실에서는 마우스 볼을 빼가는 문제로 골머리를 앓았다.

그러던 어느 날 광마우스가 나타났고,

누군가는 마우스 볼 빼가는 문제가 해결되었다고 좋아했다.

누군가는 광마우스는 너무 가벼워서 사용하는 기분이 안 난다고 사용하지 않았다.

그리고, 학교 실습실에서는 마우스가 통째로 사라지는 문제가 생겼다.

마우스가 몇 천 원짜리가 되면서 훔쳐가는 사람은 사라졌고,

어느덧 선이 사라졌다.

게임 할 때 반응속도가 느리다고, 선을 사용하는 사람도 있고

배터리가 내장되어서 무겁다고 불평하는 사람도 있었다.

그리고, 어느덧 버튼 사이에 휠(wheel)이 끼어들더니, 상하 스크롤을 할수 있게 되었다.

그렇게 하루 종일 마우스만 누르면서 일하고 있다.

오래 되었다.

 

터치 패드처럼 마우스 전체가 터치되고,
상하좌우스크롤되는 맥북용 마우스를 몇 일 사용하고서

'개발 이야기' 카테고리의 다른 글

왼쪽? 오른쪽?  (0) 2019.08.04
오만과 편견 또는 게으름  (0) 2018.11.23
개발자가 하면 안 좋은 일  (2) 2018.10.28
두 마리 소 II  (0) 2018.09.11
배틀스타 갤럭티카(Battlestar Galactica)와 직장 생활  (2) 2018.06.13

등산 중에 

반대편에서 산을 내려 오던 사람이 내 앞에 가던 사람에게 내려가는 길을 물었다.

그러자 익숙한 듯이 간단하게 대답했다.

“가다가 왼쪽으로 내려 가세요”라고 말했고,

물어보던 사람은 “왼쪽이요?”이라고 다시 물었다.

“가다가 첫 번째 길에서 왼쪽으로 가도 되고, 두 번째 길에서 빠져도 됩니다.”라고 말하고 다시 걸어 갔다.

첫 대답을 듣고 순간적으로 "왼쪽이 맞나?"라는 생각이 들었다.

길을 묻고 답할 때 흔하게 하는 실수로

가는 사람의 입장에서는 왼쪽이지만, 반대로 오는 사람의 입장에서는 오른쪽 일 수 있기 때문이다.


왼쪽이 맞았다.

상대의 입장에서 답변한 것이었다.

상대를 배려하는 마음씨에 감탄하면서 여러 가지 생각이 들기 시작했다.

 

다른 사람과 대화를 하다 보면, 항상 나의 입장에서 이야기 하게 된다.

내가 아는 건 상대도 안다고 생각하고 말하거나

단어만 뱉어놓고 아예 알아서 뭔가를 해주길 원하는 등

상대의 입장에서 이야기 할 때 보다, 나의 입장에서 이야기 하는 것이 대부분 일 것이다.

좀 심한 사람은 자신이 편한 데로 이야기 하고,

“재는 왜 그러는지 모르겠다”라는 말을 자주 하기도 한다.

직장 생활이나 갑과 을의 관계에서는 아주 흔하게 봤었다.

그렇게 길을 조금 더 갔다.

어느 길로 갈까 싶어서 표지판을 보다 그만 웃고 말았다.

직진을 하면 정상으로 가고, 우측으로 빠지면(내려 올 때는 왼쪽이 된다.) 

아까 물어보던 사람이 원하는 길이 되는 것이다.

 

길을 물어보던 사람은 갈림길을 조금 전에 지나쳐 왔고

자신이 지나친 건지 아직 도착하지 않은 것인지 몰라서 물었던 것 같았다.

길을 제대로 아는 사람이 있었다면 되돌아 가서 내려가라고 했을 것이다.

그러면 한 10~20분 정도 시간을 줄였을 것이다.

더욱이 이 갈림길로 내려가면 계곡이 있어서 쉬어가기 좋다.

(몇일전 쏟아진 폭우로 계곡의 물이 정말 좋았다.)

지나쳐서 내려가는 길은 능선을 따라 걸어가야 해서 그늘이 별로 없고 덮다.

30~40분을 가야 왼쪽으로 빠지는 길이 나오기 때문에 계속 왜 왼쪽에 길이 없지라면서 내려갔을 수도 있다.

그날찍은 사진

조금의 잘못된(?) 대화로,

목표는 이루었지만 시간과 즐거움을 제법 많이 놓친 것이다.

“지나온 것 같기도 하고, 아닌 것 같기도 한데...”라며 질문을 했다면 좀더 조심스런 대답이 있지 않았을까?

첫 번째 왼쪽으로 내려가는 길은 30~40분을 걸어가야 나오기 때문에 답변이 조심스러워 졌을 수도 있을 것이다.

답변하던 사람이나 뒤에 있던 나도 제대로 길을 좀더 알았거나 (지식이 더 있었거나)

답변을 길게 했더라면 어떠했을까 하는 생각도 들었다.

“한 40분 쭉 내려가시면, 왼쪽으로 내려 가는 길이 보입니다. 그 길을 따라 가시면 되요”

40분뒤에 내려가는 길이 나온다는 말을 들었다면 지나쳤다는 생각을 했거나,

자신의 생각과 다른 답변에 질문했던 사람이 이상하게 생각해서 더 상세하게 질문했을 것이고, 

다른 결과가 나올 수도 있었을 것이다.

답변을 듣고 얼마나 더 가야 하냐를 물어봐도 좋았을 것이다.

 

일을 하다 보면 이와 같은 서로간의 대화 문제로 항상 많은 문제가 생겼다.

몇 년 전에 SI 개발을 하면서 있었던 일이다.

어떤 기업의 시스템 구축을 하기 위해, 분석 회의를 진행하고 있었다.

해당 기업의 임원 분이 계속 엑셀 다운로드 기능이 있어야 한다고 강조 했다.

어떤 기능을 이렇게 만들어야 한다고 이야기 하고 나면, 

리스트 부분에서는 꼭 엑셀 다운로드도 있어야 한다고 강조 했다.

엑셀을 참 좋아하는 분이라고 생각하다,

어느 날 잡담 중에 지나가는 말로 “왜 그렇게 엑셀 다운로드를 모든 기능에 넣습니까?”라고 물어봤다.

“데이터가 언제 사라질지 모르니, 개인이 등록한 정보는 엑셀로 다운받아서 보관하게 해줘야 합니다.”

다른 회사에서 일하다 이직하고 처음 맡은 SI라며 

이전 회사에서는 자주 데이터가 없어지는 문제가 생기거나, 저장 공간 문제로 데이터를 삭제한다고 했다.

데이터 베이스에 데이터가 저장되면 누군가가 삭제할 수 없고, 

개인이 등록한 정보는 개인이 관리하는 기능(myPage)에서 한 눈에 볼 수 있다고 설명했고, 

엑셀 다운로드는 줄이기로 했었다.


고객은 자신의 사정과 상황에 따라 말을 하고,

개발자는 상세한 이유를 모르니 말한 데로 개발을 한다.

그렇게 시간이 지나면 안해도 될 일을 하거나,

처음엔 사소한 것들이 프로젝트 후반기로 가면 눈덩이처럼 커져서 큰 문제가 되기도 한다.

SW 개발은 내가 나에게 발주하는 상황보다,

고객이나 직장 상사와 같은 타인의 요청에 의해 이루어 지는 경우가 더 많다.

타인에 의해 진행되기 때문에

제대로 된 개발과 빠른 개발을 하기 위한 가장 중요한 기본 능력은 대화 능력인 것 같다.

왼쪽인지 오른쪽인지 각자의 위치에서 방향을 이야기 해서 힘들 수 있고,

서로 가지 않은 길을 가다 보니 경험 부족등으로 이리 저리 방황할 수도 있다.

이런 문제를 해결하는 건 서로가 서로에게 계속 물어보고 답변해 가는 것일 것이다.

하지만 현실적으로는 각자의 일이 바쁘거나, 사람이 싫거나, 대화가 싫어서 등으로 그러지 못하는 것 같고,

그렇게 아주 먼 길을 돌아가는 걸 많이 봤다.

 

계곡에 발 담그고 앉아서

'개발 이야기' 카테고리의 다른 글

옛날 옛날에  (0) 2019.08.07
오만과 편견 또는 게으름  (0) 2018.11.23
개발자가 하면 안 좋은 일  (2) 2018.10.28
두 마리 소 II  (0) 2018.09.11
배틀스타 갤럭티카(Battlestar Galactica)와 직장 생활  (2) 2018.06.13

gu-upload는 웹 사이트에서 파일을 업로드하는 HTML5 기반 JavaScript 라이브러리로 

2014년에 처음 제작하여 현재까지 조금씩 수정하여 사용하고 있다.

2016년에 수정하고 작성한 문서는 여기서 확인할 수 있고

그 동안 사용하면서 불편하거나 문제가 된 사항을 이번에 수정하면서 새롭게 문서를 작성한다.

 

gu-upload는 기본적으로 HTML5 기반이라 IE(Edge), Firefox, Chrome에서 실행된다.

그리고, IE 9 이전의 웹 브라우저에서는 플래시로 제작된 SWFUpload가 실행된다.

아직도 옛 버전의 IE를 사용하는 사람과 기입들이 있어서 기능 수정 없이

같이 제공하고 있으며, 몇 년 뒤에는 제외할 예정이다.

 

주요 기능은 다음과 같다.

  1. 탐색기에서 드래그 앤 드롭(Drag & Drop)으로 파일을 추가하거나, gu-upload를 더블 클릭하여 파일 추가.
  2. 여러 개의 파일을 선택해서 업로드 및 전송 상태 표시.
  3. 추가한 파일을 리스트나 미리보기(이미지용)로 설정 사용
  4. 기타 파일 크기나 개수 제한 기능 등

 

gu-upload의 소스는 github에서 받을 수 있고,

gu-upload을 사용하는 Java(Spring) 예제도 여기에서 받을 수 있다.

PHP등의 다른 개발 언어도 전송 받은 파일을 저장하는 부분만 다를 뿐,

gu-upload의 사용법은 모두 동일하다.

여기에서는 gu-upload을 사용하는 Java(Spring) 예제를 중심으로 gu-upload의 사용법을 정리한다.



먼저, gu-upload를 사용하기 위해서는 github에서 소스를 받아서 웹 프로젝트의 자바 스크립트 폴더에 넣어주면 된다.

( Java(Spring) 예제를 Eclipse에서 받아서 전체 코드와, 블로그의 내용을 같이 보는 것이 이해하는데 좋을 것 같다.)

guupload폴더에 있는 guuploadManager.js와 css 폴더에 있는 guupload.css가 기본 파일로

gu-upload를 사용하려는 웹 페이지에 다음과 같이 각 파일을 포함해서 사용한다.

<link rel="stylesheet" type="text/css" href="js/gu-upload/css/guupload.css"/>
<script type="text/javascript" src="js/gu-upload/guuploadManager.js"></script>

guupload.css는 gu-upload에서 사용하는 모든 디자인(CSS) 클래스를 모아놓은 파일로

gu-upload를 사용하는 사람의 취향에 맞추어 수정해서 사용하면 된다.

guuploadManager.js는 gu-upload와 SWFUpload를 관리하는 부분으로,

사용하려는 웹 페이지에 이 두 개의 파일을 포함하고, 다음과 같이 사용하면 된다.

var guManager=null;

window.onload = function() {
	var option = {
		fileid: "attachFile",
		uploadURL: "upload",
		maxFileSize: 100,
		maxFileCount: 12,
		useButtons: false,
		afterFileTransfer: afterFileTransfer
	}
	guManager = new guUploadManager(option);
}

전체 소스 코드

웹 페이지 실행이 완료된 후(onload),

guUploadManager를 적절한 옵션으로 생성(new)하면 된다.

옵션 설명
fileid gu-upload를 생성할 태그의 ID (주로 div)
listtype 첨부할 파일을 보여주는 방식으로, 리스트와 미리보기(이미지) 방식이 있다.
별도로 지정하지 않으면 리스트 방식이고, 
thumbnail로 지정하면 미리보기 기능을 이용할 수 있다.
uploadURL gu-upload로 전송한 파일을 서버에서 받아서 저장할 컨트롤 주소
maxFileSize 전송할 파일의 최대 크기. 기본값은 20M.
maxFileCount 한번에 전송할 수 있는 파일의 최대 개수. 기본값은 10개.
useButtons 파일 찾아보기 버튼 사용여부. 기본값은 true
afterFileTransfer 파일을 전송한 후 호출할 콜백 함수.

fileid, uploadURL, afterFileTransfer는 반드시 지정해야 하는 옵션이고,

나머지 옵션은 지정하지 않는 경우 기본값이 적용된다.

 

먼저, fileid는 gu-upload를 생성할 태그의 ID로 주로 div 태그를 다음과 사용한다.

        <tr>
            <td>Contents</td>
            <td><textarea name="brd_contents" cols="55" rows="5" style="width: 500px;"></textarea></td>
        </tr> 
        <tr>
            <td>Attach File</td>
            <td>
                <div id="attachFile" style="width: 500px;"></div>
            </td>
        </tr>

전체 소스 코드

div 태그의 id 로 'attachFile'을 지정하였고, fileid 옵션에 'attachFile'을 지정하다.

지정된 이 div 안에 gu-upload가 생성되어 실행된다.

 

listtype은 첨부할 파일을 보여주는 방식으로, 

단순하게 리스트로 보여주는 방식과 이미지 파일을 위한 미리보기 방식이 있다.

별도로 지정하지 않으면 리스트 방식이고, 

thumbnail로 지정하면 미리보기 기능을 이용할 수 있다.

예제 소스 보기

예제 소스 보기

uploadURL은 gu-upload가 전송한 파일을 받아서, 서버에 파일로 저장하는 컨트롤을 지정하는 부분으로

각각의 서버 언어에 맞는 url을 지정하면 된다.

gu-upload 예제에서는 다음과 같이 Spring으로 제작하였고

url이 upload이기 때문에 uploadURL에 upload로 지정하였다.

	@RequestMapping(value = "/upload")
	public void upload(HttpServletResponse response, HttpServletRequest request, @RequestParam("Filedata") MultipartFile Filedata) {
	   	SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
	   	String newfilename = df.format(new Date()) + Integer.toString((int) (Math.random()*10));
	   	
		File f = new File("d:\\" + newfilename);
		try { 
			Filedata.transferTo(f);
		   	response.getWriter().write(newfilename);
		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();
		}
	}	

소스코드

서버에 파일을 저장하는 부분은 gu-upload와 관련이 없는 부분으로,

(gu-upload는 파일을 서버로 전송하는 부분까지만 담당)

개발환경에 따라 upload.asp, upload.php, upload.do 등을 지정해서 사용하면 된다.

파일을 저장하고 나면, 반드시 반환 값(response.getWriter().write)으로 저장한 파일명을 넘겨야 한다.

파일명 중복, 보안등의 이유로 첨부 파일을 다른 이름으로 저장하는데,

이 파일 명을 반환해야 gu-upload가 저장한 파일명(filename), 파일 크기(filesize)와 조합하여 개발자가 사용할 수 있다.

 

옵션 중 가장 어려운 것이 afterFileTransfer으로 콜백함수를 지정해서 사용하는 것일 것이다.

gu-upload는 파일을 먼저 전송하고 사용자가 입력한 값을 전송하기 때문에

그림과 같이 일반적인 파일 업로드 프로그래밍 방식과 제법 큰 차이가 있다.

일반적인 웹 페이지에서는 사용자가 입력한 값들(Text)과 첨부 파일(file)을 같이 전송한다.

게시판을 예로 하면 제목, 글 내용등의 값(Text)와 첨부 파일(file)을 한 번에 서버로 전송한 뒤,

파일을 저장하고, 파일명(파일크기등)과 제목, 글 내용등을 데이터베이스에 저장한다.

 

gu-upload를 사용하는 경우에는

1. 첨부파일을 먼저 서버에 전송해서 저장하고

2. 서버에서 저장한 실제 파일명(realname)을 받아서, gu-upload가 가지고 있는 파일명(filename), 파일크기(filesize)을 조합해서 gu-upload 생성시 옵션으로 지정한 콜백함수(afterFileTransfer)로 넘겨 준다.

3. 콜백함수에서는 넘겨 받은 파일 정보를 사용자가 입력한 값과 같이 서버에 넘겨서 데이터 베이스에 저장한다.

 

따라서 일반적인 웹 페이지에서는 사용자가 저장 버튼을 선택하면,

지정된 함수(formSubmit)가 호출되어 사용자가 입력한 값을 확인하고,

form의 submit을 호출하여 입력값과 파일을 서버로 전송한다.

function formSubmit(){
	var brd_title = document.getElementById( 'brd_title' );
	if (brd_title.value==="") {
		alert("input!");
		return;
	}
	
	document.form1.submit();
}

전체 소스 코드

gu-upload를 사용하는 경우에는, 웹 페이지에서 사용자가 저장 버튼을 선택하면,

지정된 함수(formSubmit)함수가 호출되어 사용자가 입력한 값을 확인하고,

uploadFiles()를 호출하여 파일을 먼저 서버에 전송해서 저장한다.

function formSubmit(){
	var brd_title = document.getElementById( 'brd_title' );
	if (brd_title.value==="") {
		alert("input!");
		return;
	}
	guManager.uploadFiles();
}

function afterFileTransfer(realname, filename, filesize){
	var realname9 = document.getElementById( 'realname' );
	var filename9 = document.getElementById( 'filename' );
	var filesize9 = document.getElementById( 'filesize' );
	
	realname9.value = realname;
	filename9.value = filename;
	filesize9.value = filesize;
	
	document.form1.submit();
}

전체 소스 코드

파일 전송이 끝나면, 앞서 afterFileTransfer옵션으로 지정한 콜백함수(afterFileTransfer)가 실행된다.

파라미터로 전송한 파일의 실제 파일명(realname), 파일명(filename), 파일크기(filesize)가 넘어오고,

이 값을 다른 값들과 같이 서버에 전송한다(submit).

같이 전송하기 위해 다음과 같이 실제 파일명(realname), 파일명(filename), 파일크기(filesize)를 저장할 태그가 생성되어 있어야 한다.

    <form id="form1" name="form1" action="upload_save.jsp" method="post">
        ~~ 생략 ~~
        <input type="button" value="Submit" onclick='formSubmit()'  />
        <input type="hidden" id="realname" name="realname"/>
        <input type="hidden" id="filename" name="filename"/>
        <input type="hidden" id="filesize" name="filesize"/>        
    </form>

전체 소스 코드

예제에서는 저장할 데이터 베이스가 없기 때문에

클라이언트에서 전송한 값을 받아서 화면에 출력한다.

전송 파일은 2개 이상일 경우가 많기 때문에

파일을 전송한 후 콜백함수(afterFileTransfer)를 호출할때 각 파라미터의 값은 콤마(,)로 구분되어 저장된다.

<% 
String filename = request.getParameter("filename");
String realname = request.getParameter("realname");
String filesize = request.getParameter("filesize");
String[] reallist = realname.split(",");
String[] filelist = filename.split(",");
String[] sizelist = filesize.split(",");

for (int i=0; i<filelist.length; i++) {
	out.println(filelist[i] + " : " + reallist[i] + " : " + sizelist[i] + "<br/>");
}				
%>

전체 소스 코드

서버에서 이 값을 받아 사용할 경우에는

위 코드와 같이 파일명, 파일크기등의 각 값을 splite로 분할해서 사용하면 된다.

 

이상으로 gu-upload을 사용하는 Java(Spring) 예제를 중심으로 gu-upload의 사용법을 정리하였다.

향후에는 상태바, 파일 확장자 제한 기능 등을 추가할 예정이다.

 

Playground는 서울 시내에서 아이랑 갈만한 

실내외 놀이터, 박물관/미술관, 도서관등의 장소(약 3,400군데)들을 모아서, 

일목요연하게 보여 주는 웹 사이트로 

주말 같은 여유 시간에 아이랑 어디를 가야 할지 고민하는 부모들을 위해 

개인의 경험을 토대로 제작하였다.

수 많은 날을 검색으로 지새우며 놀러 갈 곳을 찾아 다니다 

집 주변에 괜찮은 시설이 제법 많다는 것을 알게 되어 정리하면서 시작하였다.

개발은 2단계로 나누어서 진행하고 있다.

현재는 1단계 개발을 완료한 상태로 서울 시내의 시설과 장소 데이터를 모아서 DB로 구축하고,

내 주변의 시설과 장소에 대한 리스트(맵)를 보여주는 것에 집중하였다.

현재는 개발 버전이라 가격이 싼 웹 호스팅을 사용해서 SSL을 적용하지 못했다.

(정확하게는 가격이 싸서 적용되지 않는 것이고, 싼 호스팅이라 동접이 많아지면 속도가 ...)

SSL이 되어야 웹 브라우저에서 제공하는 현재 위치를 찾는 기능(navigator.geolocation)을 사용할 수 있다.

따라서 나의 주변이라기 보다는 내가 지정한 위치 주변의 시설과 장소를 조회할 수 있게 구현하였다.

 

2단계는 각자의 경험을 공유하는 추천코스이다.

개인이 괜찮다고 추천하는 시설과 장소들을 묶어서 소개하는 기능으로 ,

개인의 경험을 공유하고, 의견을 공유하는 기능으로 구현하고 있다.

회원 관리 기능은 구현되어 있지만 (github에서 소스로 확인가능),

SSL이 적용되지 않아서 [개인정보보호법] 준수 문제로 운영에 배포되어 있지 않다.

현재는 제작자(관리자)만 작성 가능하다.

 

이 사이트의 소스는 Nodejs와 MariaDB를 기반으로 제작한 것으로

github에서 다운로드 받아서 샘플 데이터와 같이 실행 가능하지만

구축된 데이터는 공유하지 않는다.

데이터는 계속 구축 중으로, 

데이터 양이 너무 많아 현재는 서울만 제공하며 

원 데이터의 출처는 Playground 사이트github에 공유되어 있다.


다음으로 주요 기능에 대한 사용법을 정리한다.

 

먼저, [우리 동네] 메뉴를 실행하면 다음과 같이 지도(Daum map)가 실행된다.

기본 위치는 광화문이고, 

마커를 마우스(PC일경우)나 손가락(스마트폰)으로 움직여서 위치를 지정한다.

지도를 클릭해서 마커를 이동 시킬 수 있다.

SSL이 적용되지 않아 현재 위치를 자동으로 찾는 기능은 사용할 수 없지만

지도 좌측에 있는 [주소로 위치 검색] 기능을 이용해 이동할 수 있다.

지도 좌측에 있는 [주소로 위치 검색]을 선택하면, 그림과 같은 다이얼로그 창이 실행된다.

이 창에서 찾을 주소나 시설명을 입력하면 된다.

주소 검색기능은 Daum에서 제공하는 기능으로,

검색된 리스트에서 해당하는 장소를 클릭하면 위치(마커)가 이동된다.

위치를 지정하고 [2Km내에서 검색] 버튼을 클릭하면

다음 그림과 같이 지정된 위치의 주변에 있는 실내외 놀이터, 박물관, 도서관등의 위치가 색깔별로 표시된다.

조회된 데이터양이 많을 경우,

상단의 실내외 놀이터, 박물관, 도서관 옵션 기능을 이용해서

보고 싶은 종류만 볼 수 있다.

옵션 좌측에 있는 리스트 버튼을 클릭해서 시설과 장소 리스트를 보면서 선택할 수 있다.

지도에 표시된 마커(색깔별 원)를 클릭해서 상세한 정보를 볼 수도 있다.

 

다음은 [추천 코스] 기능이다.

특정 장소를 중심으로 갈만한 장소들을 묶어서 하나의 코스로 정리하여 보여 주는 기능으로

아직은 개발 중인 기능이며,

실행하면 다음 그림과 같이 리스트가 출력된다.

각 항목을 클릭하면 다음과 같은 상세 페이지가 실행된다.

상세 페이지는 추천하는 장소에 대한 리스트 아이콘, 소개글, 위치를 나타내는 지도로 구성되어 있다.

장소에 대한 리스트 아이콘이나 지도의 마커를 클릭하면

다음 그림처럼 각 장소에 대한 상세 정보를 확인할 수 있다.

지도의 좌측 중앙에 있는 빨간색 지도 아이콘을 클릭하면

현재 보고 있는 위치를 중심으로 2Km내에 있는 다른 장소들을 볼 수 있는

[우리 동네]로 이동한다.

 

아직은 개발 중이고, 기능이 제한적이지만

도움이 되는 사용자가 많았으면 좋겠다.

'Node.js > 기타' 카테고리의 다른 글

nineBatis (9batis)  (2) 2017.08.16
1. Node.js 프로젝트 생성 및 개발 준비  (0) 2017.08.05
2. MariaDB 기반 CRUD 게시판 만들기  (23) 2017.08.05
3. 게시판 예제 보강  (4) 2017.08.05
4. GitHub에서 Node.js 소스 가져오기  (0) 2017.08.05

퇴사하면서,

내가 만든 웹 에디터(damoEDITOR)가 잘되었으면 하는 마음으로 이 글을 작성한다.

영업자의 마인드로 작성된 소개서가 있지만

여기서는 개발자의 마인드로, 한가지 차이점을 중심으로 정리한다.

웹 에디터는 웹 브라우저에서 별도의 설치 없이 문서를 작성하게 하는 프로그램으로,

제법 많은 제품들이 있지만

오픈 소스로는 CKEditor와 NicEditor등이 대표적이고,

상용 제품으로 나모, Dext5, 태그프리 등의 3개 기업 제품이 있다.

NicEditor는 본 블로그에서 정리한 적이 있지만, 현재는 관리되지 않는 것 같다.

다양한 기능을 제공하는 CKEditor와 tinyMCE등의 오픈 소스 제품이 무료로 제공되어 폭넓게 사용되지만,

국내 공공 기관이나 대기업들에서는 상용 제품을 많이 사용한다.

상용이 조금 더 많은 기능을 제공하는 것도 있겠지만 기술 지원 등의 문제로 선호하는 것 같다.

상용은 나모, Dext5, 태그프리의 3개 기업 제품이 주도하는데,

아주 뒤늦게 우리 회사도 웹 에디터 시장의 한 귀퉁이를 파고 들면서 판매를 시작하였다.

(실제로는 자사 기업용 솔루션에서 사용하는 웹 에디터로 사용하였다.)

 

이 제품의 이름은 damoEDITOR (이름은 별로..)로 기존 상용 제품들과 거의 동일한 기능을 제공한다.

(기본 정보는 여기에서 확인할 수 있다.)

기존 상용 제품과 비교해 핵심 기능들은 동일하지만, 부가적인 기능에서 2% 부족하다.

이미지(IMG) 태그를 예로 들면,

기존 제품은 이미지 추가시 이미지 크기를 같이 입력해서 지정하지만,

damoEDITOR는 이미지를 추가하고 난 뒤에 마우스로 조절한다.

 

하지만 damoEDITOR는 한가지 차이를 가지고 제작되었고, 이 차이로 인해 몇 가지 장점이 있다.

 

대부분의 웹 에디터는(특히 상용은 모두) IFrame을 이용해서 제작되었다.

IFrame은 Inline Frame이라는 의미로 하나의 HTML문서내에서 다른 HTML문서를 보여주기 위해 사용한다.

IFrame은 웹 보안등의 문제로 사용을 제한하는 추세이지만,

오래 전에 개발된 웹 에디터는 IFrame으로 제작하는 것 외에는 방법이 없었다.

 

최근에는 IFrame 대신에 DIV로 개발하는 것이 추세가 되고 있다.

DIV로 제작할 경우 반응형 웹(Responsive Web) 페이지에 대응할 수 있는 장점이 있다.

이것이 damoEDITOR의 첫번째 장점이다.

웹 페이지(웹브라우저)의 크기에 따라 웹 에디터의 크기가 변하게 제작할 수 있다.

반면, IFrame을 기반으로 제작된 웹 에디터들은 크기 변화에 대응하는 것이 쉽지 않다.

최근(?)에 제작된 오픈소스 웹 에디터는 DIV를 이용하여 반응형 웹용으로 많이 제작되었다.

다만, 이 웹 에디터들은 제공되는 기능이 몇 가지 없다.

damoEDITOR는 다음 그림(우측 상단)에서 보는 것처럼

웹 브라우저의 크기에 따라 변할 뿐 아니라 제공되는 기능 버튼의 위치까지 변한다.

웹 에디터의 너비가 작아지면서 가려지는 버튼들을 보조 툴바를 이용해서 메뉴처럼 보여준다.

다음 그림과 같이 더 작아지면 메인 툴바를 한줄만 남기고,

가려진 버튼들은 모두 보조 툴바에서 사용 할 수 있다.

그림과 같이 모바일 전용으로 사용할 수도 있다.

 

두 번째 장점은 작성한 데로 보는 것이다.

IFrame내에서 호출되는 HTML은 IFrame 외부의 HTML과 격리되어 있다.

이 두 개의 HTML은 별개의 페이지로 전혀 다른 CSS가 사용된다.

즉, IFrame 외부의 HTML은 웹 에디터를 구매해서 사용하려는 기업의 웹 페이지이고,

IFrame 내부의 HTML은 웹 에디터의 영역으로 독립적인 영역이다.

따라서 두 개의 CSS가 사용되어 사용자에게 혼란을 야기한다.

 

HTML <a> 태그를 예로 하면,

웹 에디터에서 한 문장을 <a> 태그로 지정하면 밑줄(underline)이 생긴다.

만약, 사이트 전체에 <a> 태그의 CSS를 밑줄이 아닌 색상을 사용하도록 지정하면 

웹 에디터로 작성한 내용을 글 읽기로 볼 때, 다음 그림과 같이 다른 화면을 보게 된다.

(웹 에디터가 사용되던 초기에 이러한 문제로 클레임이 많았던 것으로 기억한다.)

위 그림은 일반 텍스트는 회색, 링크는 검은색으로 지정해서 사용한 사이트의 예제이다.



DIV로 개발한 제품은 

사용하는 웹 페이지의 CSS를 상속받아서 사용하기 때문에 이러한 문제가 생기지 않는다.

작성 화면과 조회 화면이 동일하게 보이는 것이다.

기존 제품들은 이문제를 해결하기 위해 웹에디터의 CSS에 자사 웹 페이지의 CSS를 수정해야 한다.

 

고려하지 못한 유사한 문제가 생길 경우

damoEDITOR는 damoEditor.css에서 수정하면 해당 기업의 CSS와 동일하게 맞출수 있다.

사용하는 웹 페이지가 <a> 태그에 밑줄도 색상도 아무것도 지정하지 않으면

웹 에디터 작성시 <a> 태그에 아무것도 나오지 않아 문제가 되기도 했다.

(링크에 아무런 표시를 하지 않는 사이트라면 UI에 대해서 고민을...)

이 경우 damoEditor.css에서 다음과 같이 지정해 주면 간단하게 해결된다.

.damoEditor a {
    text-decoration: underline 
}

이렇게 처리하고, 글읽기가 있는 경우 이상의 코드를 관련 부분에 수정해서 넣어줘야 한다.

그렇지 않을 경우, 글작성(웹에디터 사용)시에는 링크에 밑줄이 있는데,

글읽기에서는 없어서 혼란을 줄 수 있다.

 

다른 웹 에디터를 많이 사용해본 사람들에게는

이러한 기능이 오히려 혼동을 주기도 해서

기존 방식처럼 CSS가 별도로 운영되는 IFrame 방식도 제공한다. 

 

세 번째 장점은 제품에 포함된 damoEditor.css 파일을 수정해서

damoEDITOR를 사용하는 기업에서 마음 데로 디자인을 수정하고 개선할 수 있다는 것이다.

이것은 damoEDITOR만의 고유한 기능으로 CSS를 조금이라도 아는 사람(퍼블리셔)이 있다면,

자신의 웹 사이트에 맞춰서 다양하게 정의해서 사용할 수 있다.

이것은 damoEDITOR를 구성하는 거의 모든 디자인이 damoEditor.css에 있기 때문에 가능하다.

대부분의 웹 에디터에서 테마를 제공하지만, 

몇 가지 색상만 제공하기 때문에 

사이트 디자인과 웹에디터의 디자인이 맞지 않는 문제가 많이 있다.

 

더욱이 웹 에디터들은 딱딱한 기본 구성으로 제공되는데,

damoEDITOR는 사용하는 기업에 맞춰서 다양하게 사용할 수 있다.

 

다음 그림은 기본제공되는 사각형 색상 팔렛트를 간단한 CSS 수정을 통하여 원형으로 바꿔서 사용한 예제이다.

사용법은 개발자 메뉴얼에 정리되어 있다.

차후에는 이렇게 사용자 정의로 작성된 다양한 디자인을 정리해서 판매 옵션으로 넣는다면 좋을 것 같다.

 

이상으로 damoEDITOR의 세가지 장점을 정리하였다.

제품에 대한 설명과 체험은 여기에서 할 수 있다.

나름 GS 인증까지 받은 이 제품이 정말 많이 팔리기를 기원한다.

 


ES6(ECMAScript6)

- http://web-front-end.tistory.com/21

- https://cimfalab.github.io/deepscan/2016/07/ecmascript-6

- https://www.w3schools.com/js/js_es6.asp

- https://www.w3schools.com/js/js_es5.asp


React & Redux

- https://velopert.com/3613

- https://www.inflearn.com/course/react-%ea%b0%95%ec%a2%8c-velopert/

- https://www.inflearn.com/course/reactjs-web/

- https://reactjs.org/

- React + Redux 플로우의 이해


Router (특히 로그인 상태에 따른 처리)

https://reacttraining.com/react-router/web/guides/quick-start


Firebase

- https://firebase.google.com/docs/?hl=ko


React, Redux & Firebase

- https://chanspark.github.io/2017/12/06/Firebase-%EA%B3%B5%EB%B6%80.html

- https://www.youtube.com/watch?v=mwNATxfUsgI

- https://www.youtube.com/watch?v=z0QpTl5-0Bk

- https://www.youtube.com/channel/UCW5YeuERMmlnqo4oq8vwUpg


이상의 기초를 가지고 만든 CRUD 게시판

   1. React 게시판(CRUD) 만들기

   2. React + Redux 게시판(CRUD) 만들기

   3. React(Redux) + Firebase 게시판(CRUD) 만들기


디자인

- https://material-ui.com/ or https://material-ui.com/premium-themes/

- material-ui로 만든 템플릿 https://github.com/gujc71/project9_react

- http://react-toolbox.io/#/


이상의 기초를 가지고 만든 응용 웹

- Firebase 기반 메신저 앱 - DirectTalk9 (Web과 PC 버전)


분석해 보면 좋을 예제

- https://github.com/firebase/friendlyeats-web

- https://github.com/web-pal/react-trello-board

- https://react.rocks/


라이브러리

- https://github.com/brillout/awesome-react-components

- http://khan.github.io/react-components/


'Node.js > React' 카테고리의 다른 글

React(Redux) + Firebase 게시판(CRUD) 만들기  (2) 2018.11.04
React + Redux 게시판(CRUD) 만들기  (1) 2018.11.04
React 게시판(CRUD) 만들기 1  (4) 2018.10.28
React 게시판(CRUD) 만들기 2  (11) 2018.10.28

HTML의 VIDEO 태그로 지정한 동영상이 모바일 기기에서 재생되지 않는다는 연락을 SI 개발팀으로 부터 받았다.

1년전쯤 인터넷에서 검색하여 정리한 자료에 따르면,

웹브라우저에서 동영상을 제대로 보려면 웹브라우저에서 해당 동영상의 압축 코덱을 지원해야 한다는 것이었다.

안드로이드의 크롬에서는 문제가 없었다.

안드로이드의 삼성 브라우저에서는 MP4 파일을 H264로 생성할 때 baseline 1.3으로 해야 한다고 한다.

아이폰의 사파리에서는 플레이가 되어야 했지만 되지 않았다.

다시 인터넷으로 자료를 찾아보니 또 다른 방식으로 작성해야 한다고 했다.

문제의 동영상을 확인하니 baseline 2.0으로 생성한 파일이라, 다시 작성하라고 말했다.

그리고, 아이폰은 안되는 것 같으니 국내에서 많이 사용하는 안드로이드를 선택하라고 말했다.

둘다 지원되려면 미디어 서버를 구축하거나 두개의 동영상을 생성해서 처리하라고 말했다.


SI 개발팀 팀장이 이해가 되지 않는다며 해당 동영상을 코덱별로 테스트 해보고, 좀더 찾아보겠다고 했다.

그리고, 다음날 직접 접근하면 코덱과 관계없이 안드로이드와 아이폰에서 재생된다는 것을 찾았다.

보안을 위해 첨부한 파일은 웹 폴더가 아닌 다른 폴더에 저장하고

Java 프로그램에서 해당 파일을 읽어서 전송해 주는 방식에 문제가 있다는 것이었다.

해당 파일을 웹으로 직접 접근하면 문제가 없었다.

내가 틀린 것이었다.


일을 하다 보면 틀릴 수도 있고, 맞을 수도 있다.

하지만 회사 내에서 나름 기술을 책임지고 있는 사람이 틀린 것이고, 나태한 태도에 부끄러웠다.


SI 개발팀 팀장의 접근법은 원래 내가 사용하던 방법이다.

뭔가 문제가 생겼을 때는 누군가 맞다고 이야기해도, 전에 내가 해 봤어도 처음부터 하나씩 해봐야 한다.

동영상 코덱은 웹브라우저에서 지원해야 한다는 편견으로 제대로 테스트 해보지 않고,

간단한 검색만 하고, 원래 안되라며 넘어갔다.


더욱이 검색한 자료에 아이폰의 사파리는 H264를 지원한다고 직접 정리했는데

Baseline 3.1으로 해야 한다는 다른 검색 결과를 보고 그냥 안 된다고 말한 것이다.

상반되는 결과가 있으면 한 번 더 확인해야 했었고, 맘에 걸렸지만 넘어갔다.


삼성 웹 브라우저도 삼성 매뉴얼에서 확인했지만 1년이 지난 내용이었다.

1년전에 정리했지만, 조사한 자료가 몇 년 된 것들이었다.

바빠서 자료를 제대로 조사하지 않았었고 나중에 보강해야 지라며 넘어갔었던 것이다.


Java로 파일을 읽어서 다운로드 하는 것도, 혹시 라는 생각만 하고 이런 저런 일로 핑계대며 확인하지 않은 것이다.

코덱에만 신경을 썼다.


결과는

잔뜩 흐린 날 혼자 부끄러운 하루를 보내는 것이었다.

하루 종일 흥겨워하며 돌아다니는 누군가를 봐야했다.

'개발 이야기' 카테고리의 다른 글

옛날 옛날에  (0) 2019.08.07
왼쪽? 오른쪽?  (0) 2019.08.04
개발자가 하면 안 좋은 일  (2) 2018.10.28
두 마리 소 II  (0) 2018.09.11
배틀스타 갤럭티카(Battlestar Galactica)와 직장 생활  (2) 2018.06.13

React로 작성한 이전 예제는 게시판 데이터를 배열에 저장하기 때문에 다른 사람은 볼 수 없고, 웹 브라우저를 갱신(F5)하면 초기화 되었다.

이번에는 Firebase를 이용하여 실제 게시판처럼 데이터를 서버에 저장해서 데이터가 사라지지 않고 다른 사람과 공유할 수 있도록 구현한다.


여기서 정리한 내용은 React 게시판 만들기 시리즈의 세번째 내용이다.

   1. React 게시판(CRUD) 만들기

   2. React + Redux 게시판(CRUD) 만들기

   3. React(Redux) + Firebase 게시판(CRUD) 만들기


Firebase는 구글에서 제공하는 서버 관련 기능들로 gmail계정으로 간편하게 사용할 수 있다.

NodeJS에서 Firebase를 사용하는 방법은 이 문서에 정리되어 있다.

Firebase의 콘솔 사용법도 같이 정리 되었으니 이전 문서를 확인하고,

여기서는 Firebase의 여러 서비스 중 데이터를 저장하는 Cloud FireStore를 이용한다.

여기서 만드는 게시판은 로그인기능이 없기 때문에 아무나 글을 쓰고 저장할 수 있도록 Cloud FireStore를 테스트 모드로 설정하고, 관련 설정은 이 문서를 참고하면 된다.


배열이 아닌 Cloud FireStore에 사용자가 작성한 게시물을 저장한다.

이외에 Firebase에 대한 사용법은 Firebase 문서를 참고하고, 이 문서 중에서 Cloud FireStore 관련 문서(데이터 가져오기, 데이터 저장등)를 읽어 보는 것이 좋다.


React로 작성한 이전 게시판 예제Nodejs 기반으로 작성한 Firebase 게시판 예제를 합치는 것으로,

다음과 같이 App_reduce.js 파일의 내용만 Firebase에 맞추어 조금만 수정하면 작성할 수 있다.

그전에 다음과 같이 Firebase와 미들웨어를 설치 한다.

npm install --save firebase

npm install --save redux-thunk

redux-thunk를 설치하고, redux-thunk를 사용할 수 있도록 다음과 같이 index.js 파일을 수정한다.

import thunk from 'redux-thunk';

import App from './App';
import board_reducer from './App_reducer';

let store = createStore(board_reducer
                , window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
               , applyMiddleware(thunk) );

index.js

그리고, Firestore.js 파일을 생성하여 다음과 같이 작성한다.

import firebase from 'firebase';

var config = {
    apiKey: "AIzaSyDbsU_",
    authDomain: "fir-example-.firebaseapp.com",
    databaseURL: "https://fir-example-.firebaseio.com",
    projectId: "fir-example-",
    storageBucket: "fir-example-.appspot.com",
    messagingSenderId: ""
};
firebase.initializeApp(config);
var firestore = firebase.firestore();

const settings = {timestampsInSnapshots: true};
firestore.settings(settings);

export default firestore;


Firestore.js

React에서 Firestore에 접속하기 위한 정보로 config의 내용을 채워야 한다.

config의 내용은 Firebase 콘솔에서 가져올 수 있고, 가져오는 방법은 이전 문서를 참고하면 된다.

Firestore에 접속한 연결은 firestore로 외부로 보내지고 (export ),

다음 코드에서처럼 firestore로 사용된다 [라인 2, 7, 23, 32, 40].


다음과 같이 App_reduce.js에 firebase_board_list, firebase_board_remove, firebase_board_save 3개의 함수를 추가한다.

3 개의 파일은 Firebase(FireStore)의 게시판 데이터를 가지고 오는 리스트, 선택한 게시물을 삭제하는 삭제, 게시글을 수정하거나 새로 등록하는 글 저장 함수이다.

먼저, firebase_board_list()는 FireStore에서 데이터를 가지고 와서 기존의 state.boards에 넣어주는 기능을 한다 [라인 5].

작성된 날짜(brddate)를 기준으로 최근 데이터가 먼저 오게(desc) 데이터들을 가지고 와서 (get) [라인7 ]

데이터의 개수 만큼 반복(forEach)해서 별도의 배열(rows)에 넣어주고 [라인 10],

화면에 출력하기 위해 board_list() 함수를 호출해서 Redux로 보관한다 [라인 15].

데이터의 개수 만큼 반복하는 것은 전송 받은 데이터 리스트 구조가 JSon 배열이라 필요한 정보만 추출(doc.data() – 게시물 하나)해서 보관하기 위한 것이다 [라인 11].

전송 받은 데이터를 그대로 배열에 넣어서 사용해도 된다 (대신 관련한 코드를 좀더 수정해야 한다.).

이전 예제에서는 board_list()함수를 사용하지 않았는데, 여기서는 state.boards에 데이터를 저장하는 역할을 한다.

개념상 Redux로 저장된 state.boards의 데이터를 가지고 오는 것이 되어야 할 것 같지만 반대로 저장하는 역할을 한다.

즉, firebase_board_list()로 서버에서 데이터를 가지고 오고, board_list()로 state에 저장하는 구조이다.


firebase_board_remove()는 주어진 글번호로(brdno) FireStore에 있는 데이터를 삭제(delete)하는 명령어를 실행한다 [라인 20].

삭제하고 나면(then) [라인 23],

화면에 출력하기 위해 데이터를 가지고 있는 state.boards에서 제거하기 위해 board_remove() 함수를 호출한다 [라인 24].

board_remove()는 기존 코드와 동일하다.


firebase_board_save()함수는 수정인지 신규 등록인지 구분해서 [라인 31]

수정이면 update [라인 10],

신규 등록이면 set으로 데이터를 저장한다 [라인 35].

수정이든 신규등록이든 저장하고 나면(then), board_save() 함수를 호출해서 state.boards의 값을 수정하여 화면에 반영되게 한다 [라인 37, 41].

board_save()의 코드도 기존 코드와 동일하다.


사용자가 행을 선택하면 호출하는 board_read()는 데이터 변경이 없기 때문에 수정없이 그대로 사용한다.


initialState 에서 선언했던 최대 글번호(maxNo)를 제거한다 [라인 47].

Firestore에 저장하기 때문에 Firestore에서 제공하는 문서 번호(id)를 글번호로 사용한다 [라인 33].


이상으로 App_reducer.js 파일을 이렇게 수정한 뒤,

기존에 각 컴포넌트에서 사용한 board_remove(), board_save() 함수를 사용한 각 컴포넌트에서 firebase_ 을 앞에 붙여 추가한 3개의 함수를 사용하도록 하고,

글 리스트의 경우에는 state.boards 를 사용하기 전에 firebase_board_list()를 호출하도록 수정한다.


글리스트(BoardList) 먼저 수정하면 다음과 같다.

App.js에 작성했던 리스트 코드를 분리하여 별도의 컴포넌트로 제작하였다.

이해를 위해 App.js에 그대로 작성했지만, React에서는 기능별로 세분화해서 컴포넌트로 사용하는 것이 권장된다.


글 리스트를 구현 할 것이기 때문에 Reducer에 작성해 놓은 firebase_board_list()를 사용한다고 선언한다(import) [라인 2].

컴포넌트가 생성되는 시점에 발생하는 이벤트인 componentDidMount에서 firebase_board_list()를 실행한다 [라인 6].

firebase_board_list()를 호출하면 Reducer의 state.boards에 게시글들이 채워지고

이것을 바라보고 있는 this.props.boards에 전달되면서 화면에 출력된다 [라인 9].

this.props.boards는 앞서 정리한 데로 mapStateToProps으로 연결하였다 [라인 16~23].


각 게시물을 출력하는 BoardItem은 데이터 삭제를 위해 board_remove() 대신에 firebase_board_remove()를 호출한다.

import { board_read, firebase_board_remove } from './App_reducer'

const BoardItem = ({row, inx, board_read, firebase_board_remove}) => (
    <tr>
        <td>{inx}</td>
        <td><a onClick={() => { board_read(row.brdno) } }>{row.brdtitle}</a></td>
        <td>{row.brdwriter}</td>
        <td>{row.brddate}</td>
        <td><a onClick={() => { firebase_board_remove(row.brdno) }}>X</a></td>
    </tr>
);

const mapDispatchToProps = dispatch => ({
  board_read: brdno => dispatch(board_read(brdno)),
  firebase_board_remove: brdno => dispatch(firebase_board_remove(brdno))
})

export default connect(null, mapDispatchToProps)(BoardItem)

BoardItem.js

코드의 다양성을 보여 주기 위해 BoardItem에서는 기존과 다른 방법으로 App_reducer의 함수를 호출하도록 했다.

BoardList 컴포넌트에서는 import문에서 사용할 함수를 선언하고 다음과 같이 사용했다.

         this.props.dispatch(firebase_board_list());

firebase_board_remove도 이렇게 사용해도 되지만 mapDispatchToProps 를 사용해서

App_reducer에서 사용할 함수를 다시 선언하고 BoardItem에 연결(connect)할 때 파라미터를 넘기는 방식으로 사용한다.

이렇게 사용하면 this.props.dispatch를 생략하고 firebase_board_remove() 함수만 호출해서 사용할 수 있다.


[ 참고 ] connect에서 첫번째 파라미터는 mapStateToProps로 컴포넌트에서 사용할 변수들을 선언한다. BoardItem에서는 사용할 변수가 없어서 null로 지정하였다. mapDispatchToProps 는 사용할 함수를 선언하는 것이다.


글 쓰기 폼(BoardForm)에서는 board_save() 대신에 firebase_board_save로 바꾸어 주면 된다.

import { firebase_board_save } from './App_reducer'

class BoardForm extends Component {
   
    handleSave = () => {
        this.props.dispatch(firebase_board_save(this.state));
        this.setState (this.initialSelectedBoard);
    }
}

BoardForm.js

BoardForm에서는 this.props.dispatch로 호출했다.

mapDispatchToProps와 this.props.dispatch 중 적절한 방식으로 작성하면 되지만 mapDispatchToProps가 많이 사용된다.

(개인적으로 귀찮아서...)


이상으로 간단하게 Redux 예제를 Firebase Cloud Firestore로 구현하였다.

기존 코드에 Firestore의 CRUD를 처리하는 함수를 추가하는 것으로 간단하게 구현하였다.

즉, React(Redux)로 사용자의 PC에서 화면에 데이터를 출력하고 관리하는 기능을 하도록 하고,

Firestore로 서버에 데이터를 저장하고 관리하는 기능을 하도록 서로의 역할을 나눠서 사용한다.


다른 브라우저로 접속해서 데이터가 동일하게 출력되는 것을 확인 할 수 있다.


Firestore(Cloud Firestore)는 Realtime database와 같이 실시간 데이터베이스 이다.

서버의 데이터에 변경이 생기면 현재 접속한 클라이언트에 실시간으로 데이터가 보내진다.

실시간으로 동기화 된다는 것으로, 이 기능을 이용하여 많은 예제들이 Fireabse 기반 메신저를 제작하였다.

상세한 설명은 Firebase 문서에서 확인할 수 있다.


이번에는 앞서 작성한 예제를 실시간 데이터 방식으로 작성한다.

소스는 Github의 step2 브랜치에서 받을 수 있다.

변경 방법을 간단하게 정리하면, 데이터 변경 사항을 글 리스트에서 받아서 state.boards에 반영하여 보여주는 식으로 작성한다.

코드에서 보듯이 글 리스트(firebase_board_list)에서 거의 모든 기능이 구현된다.


이전에는 get() 메소드를 사용했지만, onSnapshot()을 사용하고 [라인 4],

전송 받은 데이터의 개수(forEach) 만큼 반복했다.

이전과 가장 큰 차이는 전송된 데이터의 종류(change.type)이다 [라인 7, 11, 14].

현재 데이터(행)가 신규 추가인지(add), 수정 인지(modified), 삭제 인지를(removed) 나타낸다.

이 종류에 따라 board_save()나 [라인 9, 12] board_remove()를 호출해서[라인 15] state.boards의 데이터를 관리한다.


그리고, firebase_board_remove(), firebase_board_save()에서

처리 후 콜백(then)으로 board_save()나 board_remove()를 호출할 필요가 없어져 제거한다 [라인 25, 35, 37].

이 코드들을 모두 리스트에 작성하였다 [라인 9, 12, 15] .


그리고 onSnapshot으로 호출된 콜백 함수는 함수가 종료 되어도 계속 실행되기 때문에 서버의 데이터 변경 정보를 계속 수신해서 반영한다.

다른 사람이 작성한 것도 모두 실시간으로 제공된다.


 주의  onSnapshot으로 호출된 콜백 함수는 계속 실행되기(Listening) 때문에 필요하지 않을 경우에는 사용을 중지해야 한다.

중지 방법은 Firestore 문서에서 [리스너 분리]항목을 참조하면 된다.


이상으로 React, Redux, Firebase 순으로 기초적인 게시판 만들기를 정리하였다.

이해를 위해 Table 테그로 디자인 없이 작성하였는데,

그림과 같이 Material-ui와 같은 React용 디자인 라이브러리를 사용하면 제법 쓸만한 SPA(Single Page Application) 웹사이트를 만들 수 있다.

게시판에 대한 개념이 이해가 되었으면,

Google's Material Design을 준수하는 Material-ui등의 디자인 라이브러리를 적용하여 다음과 같이 사용하면 된다.


이상의 게시판에 기능을 보강하고

Fireabse 인증(로그인) 기능, Material-ui 등을 추가하여 Project9 템플릿을 제작하였다.

실제 프로젝트를 쉽게 할 수 있도록 만든 템플릿으로 (작성된 코드를 Copy & Paste 하여 개발하기 위한 예제들)

이상의 개념을 가지고 응용하는 방법을 이해하는데 도움을 줄 수 있으니 확인해보면 도움이 될 것이다.


Project9 템플릿을 기반으로 DirectTalk9 이라는 실시간 메신저를 웹 버전으로 제작하였고,

다시 이것을 Electron을 이용하여 PC 버전으로 제작하였다.

React Native를 쓰면 모바일까지….



'Node.js > React' 카테고리의 다른 글

React 학습자료  (0) 2019.01.23
React + Redux 게시판(CRUD) 만들기  (1) 2018.11.04
React 게시판(CRUD) 만들기 1  (4) 2018.10.28
React 게시판(CRUD) 만들기 2  (11) 2018.10.28

+ Recent posts