-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.html
More file actions
818 lines (688 loc) · 279 KB
/
Copy pathsearch.html
File metadata and controls
818 lines (688 loc) · 279 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
<!DOCTYPE html>
<html>
<head>
<!-- Document Settings -->
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Base Meta -->
<!-- dynamically fixing the title for tag/author pages -->
<title>Search Result</title>
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Styles'n'Scripts -->
<link rel="stylesheet" type="text/css" href="/assets/built/screen.css" />
<link rel="stylesheet" type="text/css" href="/assets/built/screen.edited.css" />
<link rel="stylesheet" type="text/css" href="/assets/built/syntax.css" />
<!-- custom.css -->
<link rel="stylesheet" type="text/css" href="/assets/built/custom.css" />
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- 웹폰트 추가 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/earlyaccess/nanumgothic.css">
<!-- highlight.js -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<style>.hljs { background: none; }</style>
<!--[if IE]>
<style>
p, ol, ul{
width: 100%;
}
blockquote{
width: 100%;
}
</style>
<![endif]-->
<!-- This tag outputs SEO meta+structured data and other important settings -->
<meta name="description" content="기술 블로그" />
<link rel="shortcut icon" href="https://simplehanlab.github.io/assets/built/images/favicon.jpg" type="image/png" />
<link rel="canonical" href="https://simplehanlab.github.io/search" />
<meta name="referrer" content="no-referrer-when-downgrade" />
<!--title below is coming from _includes/dynamic_title-->
<meta property="og:site_name" content="SimpleHan Lab" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Search Result" />
<meta property="og:description" content="기술 블로그" />
<meta property="og:url" content="https://simplehanlab.github.io/search" />
<meta property="og:image" content="https://simplehanlab.github.io/assets/built/images/blog-cover1.png" />
<meta property="article:publisher" content="https://www.facebook.com/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Search Result" />
<meta name="twitter:description" content="기술 블로그" />
<meta name="twitter:url" content="https://simplehanlab.github.io/" />
<meta name="twitter:image" content="https://simplehanlab.github.io/assets/built/images/blog-cover1.png" />
<meta name="twitter:label1" content="Written by" />
<meta name="twitter:data1" content="SimpleHan Lab" />
<meta name="twitter:site" content="@" />
<meta name="twitter:creator" content="@" />
<meta property="og:image:width" content="2000" />
<meta property="og:image:height" content="666" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Website",
"publisher": {
"@type": "Organization",
"name": "SimpleHan Lab",
"logo": "https://simplehanlab.github.io/"
},
"url": "https://simplehanlab.github.io/search",
"image": {
"@type": "ImageObject",
"url": "https://simplehanlab.github.io/assets/built/images/blog-cover1.png",
"width": 2000,
"height": 666
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://simplehanlab.github.io/search"
},
"description": "기술 블로그"
}
</script>
<!-- <script type="text/javascript" src="https://demo.ghost.io/public/ghost-sdk.min.js?v=724281a32e"></script>
<script type="text/javascript">
ghost.init({
clientId: "ghost-frontend",
clientSecret: "f84a07a72b17"
});
</script> -->
<meta name="generator" content="Jekyll 3.6.2" />
<link rel="alternate" type="application/rss+xml" title="Search Result" href="/feed.xml" />
</head>
<body class="page-template">
<div class="site-wrapper">
<!-- All the main content gets inserted here, index.hbs, post.hbs, etc -->
<!-- < default -->
<!-- The tag above means: insert everything in this file
into the {body} of the default.hbs template -->
<!-- The big featured header, it uses blog cover image as a BG if available -->
<header class="site-header outer">
<div class="inner">
<nav class="site-nav">
<div class="site-nav-left">
<a class="site-nav-logo" href="https://simplehanlab.github.io/">SimpleHan Lab</a>
<ul class="nav" role="menu">
<li class="nav-about" role="menuitem"><a href="/about/">About</a></li>
<li class="nav-jekyll" role="menuitem"><a href="/category/react/">react</a></li>
<li class="nav-python" role="menuitem"><a href="/category/spring/">spring</a></li>
<li class="nav-java" role="menuitem"><a href="/category/ios/">ios</a></li>
<li class="nav-jekyll" role="menuitem"><a href="/category/jekyll/">jekyll</a></li>
<li class="nav-python" role="menuitem"><a href="/category/minimal-mistakes/">minimal-mistakes</a></li>
<li class="nav-python" role="menuitem"><a href="/category/ssl/">ssl</a></li>
<li class="nav-python" role="menuitem"><a href="/category/tensorflow/">tensorflow</a></li>
<li class="nav-python" role="menuitem"><a href="/category/ssh/">ssh</a></li>
<li class="nav-archive" role="menuitem">
<a href="/archive.html">All Posts</a>
</li>
<li class="nav-archive" role="menuitem">
<a href="/categories.html">카테고리별 Posts</a>
</li>
<li class="nav-archive" role="menuitem">
<a href="/author_archive.html">Tag별 Posts</a>
</li>
</ul>
</div>
<div class="site-nav-right">
<div class="social-links">
</div>
<a class="subscribe-button" href="#subscribe">Search</a>
</div>
</nav>
</div>
</header>
<!-- Everything inside the #post tags pulls data from the post -->
<!-- #post -->
<main id="site-main" class="site-main outer" role="main">
<div class="inner">
<article class="post-full post page no-image">
<header class="post-full-header">
<h1 class="post-full-title">Search Result</h1>
</header>
<section class="post-full-content">
<form action="/search" method="get" hidden="hidden">
<label for="search-box"></label>
<input type="text" id="search-box" name="query">
</form>
<ul class="mylist" id="search-results"></ul>
<script>
window.store = {
"mavengradle": {
"title": "Maven과 Gradle 차이",
"author": "Simplehan",
"category": "",
"content": "빌드 도구란? 빌드 도구는 빌드 자동화를 시켜주는 실행가능한 프로그램만들어 주는 프로그램 ex) maven, gradle, ant빌드 자동화란? 빌드 자동화의 본질적 목적은 소프트웨어 개발자가 반복해서 하는 코딩을 잘 짜여진 프로세스를 통해 자동으로 실행하여, 믿을 수 있는 결과물도 생산해 낼 수 있는 일련의 작업방식 및 방법을 말한다. 지속적인 통합(continuous Integration)과도 일맥상통되는 의미다. 수없이 반복해서 최종 결과물을 만들어내야 하는 소프트웨어 개발업무에서 빠질 수 없는 개념적 이해가 되었다. 특히 오늘날의 복잡하고 광범위한 협업적 개발 과정에서는 과정과정에서 있을 수 있는 인간적 실수의 가능성을 최소한으로 줄여야 한다. 출처 : WikipediaMaven 이란? Project object model (POM)개념의 기반으로 만들어져있으며 빌드 중인 프로젝트, 빌드 순서, 다양한 외부 라이브러리의 종속성 관계들을 pom.xml파일에 명시한다. 외부저장소에서 필요한 라이브러리와 플러그인들을 다운로드 한 다음, 로컬시스템의 캐시에 모두 저장한다.Project Object Model (POM)Project Object Model는 이름 그대로 Project Object Model의 정보를 담고있는 파일이다. 이 파일에서 주요하게 다루는 기능들은 다음과 같다. 프로젝트 정보 : 프로젝트의 이름, 개발자 목록, 라이센스 등 빌드 설정 : 소스, 리소스, 라이프 사이클별 실행한 플러그인(goal)등 빌드와 관련된 설정 빌드 환경 : 사용자 환경 별로 달라질 수 있는 프로파일 정보 POM연관 정보 : 의존 프로젝트(모듈), 상위 프로젝트, 포함하고 있는 하위 모듈 등Gradle 이란? Maven과 Ant에서 볼수 있는 개념들을 사용하는 대안으로써 나온 프로젝트 빌드 관리 툴이다. Ant처럼 유연한 범용 빌드 도구 Maven을 사용할 수 있는 변환 가능 컨벤션 프레임 워크 Groovy 언어를 사용한 Domain-specific-language를 사용한다. (설정파일을 xml파일을 사용하는 Maven보다 코드가 훨씬 간결하다.) 2007년에 처음 개발되었고, 2013년에 구글에 의해 안드로이드 프로젝트의 빌드 시스템으로 채택되었다.Maven Gradle 차이 Maven은 고정적이고 선형적인 단계의 모델을 기반Gradle은 작업 의존성 그래프를 기반 Maven, Gradle 둘 다 다중 모듈 빌드를 병렬로 실행할 수 있지만Gradle은 어떤 task가 업데이트 되었고 안되었는지를 체크하기 때문에 incremental build를 허용한다.이미 업데이트된 task에 대해서는 작업이 실행 되지 않으므로 빌드 시간이 훨씬 단축된다. Maven은 멀티 프로젝트에서 특정 설정을 다른 모듈에서 사용하려면 상속을 받아야하지Gradle은 설정 주입 방식을 제공한다. Gradle은 concurrent에 안전한 캐시를 허용한다.2개 이상의 프로젝트에서 동일한 캐시를 사용할 경우, 서로 overwrite되지 않도록 checksum기반의 캐시를 사용하고 캐시를 repository와 동기화시킬 수 있다.Maven 코드 예제<?xml version=\"1.0\" encoding=\"UTF-8\"?><project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"><modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-maven</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo-maven</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>Gradle 코드 예제buildscript { ext { springBootVersion = '1.5.4.RELEASE' } repositories { m avenCentral() } dependencies { classpath(\"org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}\") }}apply plugin: 'java'apply plugin: 'idea'apply plugin: 'org.springframework.boot'version = '0.0.1-SNAPSHOT'sourceCompatibility = 1.8repositories { mavenCentral()}dependencies { compile('org.springframework.boot:spring-boot-starter') testCompile('org.springframework.boot:spring-boot-starter-test')}",
"url": "/mavengradle"
}
,
"python-control-statement": {
"title": "Python 강좌(2) - Python 제어문",
"author": "Simplehan",
"category": "",
"content": "Python 강좌는 여러 절로 구성되어 있습니다. Python 강좌(1) - Python 기본 Python 강좌(2) - Python 제어문이 글은 Python 제어문 입니다.!!",
"url": "/python-control-statement"
}
,
"python-base": {
"title": "Python 강좌(1) - Python 기본",
"author": "Simplehan",
"category": "",
"content": "Python 강좌는 여러 절로 구성되어 있습니다. Python 강좌(1) - Python 기본 Python 강좌(2) - Python 제어문이 글은 python base 입니다.!!function syntaxHighlight(code) { var foo = 'Hello World'; var bar = 100;}",
"url": "/python-base"
}
,
"pmd": {
"title": "PMD 설치 및 사용방법",
"author": "Simplehan",
"category": "",
"content": "※\tPMD 란 JAVA 소스 코드를 분석하여 프로그램의 오류 또는 불필요한 부분을 찾아내고 성능을 높이는 오픈 소스 도구 PMD는 미사용 변수, 비어있는 코드 블락, 불필요한 오브젝트 생성과 같은 결함을 유발할 수 있는 코드를 검사한다. 자바 코드를 검사해서 나쁜 코딩을 잡아주고 그에 대한 설명을 지원하며, Eclipse, Jbuider 등에 연동하여 사용한다.※ PMD 설치방법 이클립스와 연동을 위해 Help → Install New Software 클릭한다 Name을 입력하고 다운로드 주소를 입력한다 http://sourceforge.net/projects/pmd/files/pmd-eclipse/update-site 다운로드 시작 다운로드 완료 항목 선택 후 설치 시작 라이선스 동의 후 완료※ PMD 사용방법 코드 검사할 파일 우클릭 → PMD → Check code 코드 검사 완료 항목 우클릭 → show details 불량 코드로 지적된 사항 확인※ PMD 룰셋• PMD 룰셋이란전자정부 표준프레임워크에서는 Code Inspection 을 위한 룰셋으로 논리오류/구문오류/참조오류 영역을 대상으로 하는 총 39개의 룰을 표준으로 선정하였다• 룰셋 환경설정Eclipse IDE의 환경설정을 통해, 전자정부 표준프레임워크 표준 Inspection 룰셋을 PMD Plugin에 적용할 수 있으며, 다음과 같은 순서로 수행한다. 전자정부 표준프레임워크 표준 Inspection 룰셋 PMD를 이용한 Code Inspection 시 기준이 되는 요소는 룰이며, 전자정부 표준프레임워크에서는 PMD가 제공하는 수 많은 룰 중에 기본이 될 만한 것들을 간추려 전자정부 표준프레임워크 표준 Inspection 룰셋이라는 이름으로 구성하였다. 이는 39개의 룰들로 구성되며, 개별 룰에 대한 상세 설명은 다음을 참조하라⊙ https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:dev2:imp:inspection:usetool#%EC%A0%84%EC%9E%90%EC%A0%95%EB%B6%80%ED%91%9C%EC%A4%80_inspection%EB%A3%B0%EC%85%8B 다음 두가지 경로 중, 하나를 선택해어 전자정부 표준프레임워크 표준 Inspection 룰셋을 다운 표준 Inspection 룰셋 한글/영문판의 압축파일 : 개발환경 3.5 이상 버전 사용 전자정부 표준프레임워크 온라인 지원 포탈사이트의 다운로드 > 개발환경 > 구현도구 소스코드에서 내려받기 내려받은 파일을 임의의 디렉터리에 압축 해제 Eclipse IDE의 메뉴에서 Window → Preferences 선택 Preferences 창의 왼쪽 메뉴 구조에서, PMD → Rules Configuration 선택 Preferences 창에서, use global rule management 선택 후 ruleset 목록 전체선택후 우측의 [x] Remove rules 아이콘 클릭 Preferences 창에서, 우측의 Import ruleset 아이콘 클릭 Ruleset 가져오기 팝업창에서, 우측의 Browse…버튼 클릭한후 앞서 압축해제한 룰셋 파일 중 한글판인 EgovInspectionRules_kor-3.5.xml 파일을 선택 Ruleset 가져오기 팝업창에서, ruleset목록이 확인되면 OK버튼을 클릭 Perferences 창에서, 목록 좌측의 체크박스를 모두 선택하고 OK버튼을 클릭 룰셋을 적용하겠냐는 질문에 YES버튼을 클릭하여 최종 확인. ",
"url": "/pmd"
}
,
"tortoisegit-2": {
"title": "TortoiseGit 사용방법",
"author": "Simplehan",
"category": "",
"content": "※\tTortoiseGit 사용방법 데이터 받아오기 (Git 데이터 복제)Git 자료를 복제 받기 원하는 곳으로 이동한 후 마우스 우클릭 “Git 복제하기..” 클릭새로 생긴 복제창에서 “URL” 부분에 복제할 자료의 Repository 주소를 입력한다권한을 가진 Git 사용자 정보를 입력한다Git 데이터 복제Git 데이터 복제 완료 데이터 업데이트 (Git 데이터 동기화)데이터 복제된 폴더에서 우클릭 “Git 동기화..” 클릭“가져와 병합하기” 클릭업데이트 된 내역이 출력됨. 다시 “가져와 병합하기” 클릭데이터가 업데이트 되고 변경된 내역이 출력됨 데이터 저장 (Git 작업 Commit)수정한 파일이 위치한 폴더에서 우클릭 “Git 커밋 -> master” 클릭작업 후엔 항상 “Commit” 을 해야한다.메시지 부분에 자신이 작업한 내용을 간략히 기입한다하단의 파일 리스트에서 자신이 작업한 파일들을 선택하고 커밋을 누른다커밋 완료. 위 상태에서는 본인 PC에만 저장되고, 서버에는 올라가지 않은 상태이다작업한 폴더에서 우클릭 “TortoiseGit → Git Push” 선택로컬, 원격 모두 master 인지 확인 후 확인버튼 선택서버에 작업내용 “Push” 완료",
"url": "/tortoisegit-2"
}
,
"tortoisegit-1": {
"title": "TortoiseGit 설치방법",
"author": "Simplehan",
"category": "",
"content": "※\tTortoiseGit 설치방법●\tTortoiseGit 이란윈도우에서 GUI환경을 이용해서 Git을 제어할 수 있도록 도와주는 버전관리 시스템이다때문에 Git은 먼저 설치되어 있어야 한다 ●\tGit 설치방법⊙ Git SCM에 접속하여 설치 파일 다운로드https://git-scm.com/Git을 사용할 기본 에디터 선택 (기본설정 Vim)https 전송 인증서 선택 – 기본설정으로 NextOpenSSL 라이브러리 사용 – 기본설정으로 NextGit Bash 터미널 에뮬레이터 설정Use MinTTY(the default terminal of MSYS2) – Git bash 기본 터미널 사용기본설정으로 NextEnable file system caching – 빠른 실행을 위해 데이터를 메모리에 캐시설정이 완료되면 인스톨Launch Git Bash에 체크하고 Finish 하면 Git Bash가 실행된다특정 폴더에서 우클릭시에도 Git Bash를 실행할 수 있다●\tTortoiseGit 설치방법 TortoiseGit 다운로드Windows 환경에 맞는 설치 파일을 다운로드https://tortoisegit.org/download/ 설치Tortoise에서 제공하는 기본 PuTTY 기반의 SSH를 선택설치 준비가 완료되면 Install 진행첫번째 언어선택 화면에서 사용 언어를 선택추가할 언어팩은 아래 주소에서 추가할 수 있다다운로드 : https://tortoisegit.org/download/Git의 실행파일 위치를 입력해야 한다. Git을 먼저 설치하였다면 자동으로 등록된다유저 정보 입력설치 완료",
"url": "/tortoisegit-1"
}
,
"gitlab-install-2": {
"title": "CentOS 7 Gitlab 설치 및 설정(2) - Gitlab 설정",
"author": "Simplehan",
"category": "",
"content": "CentOS 7 Gitlab 설치 및 설정(2) - Gitlab 설정OS : CentOS 7VERSION : gitlab-ce (community edition - 무료버전)CentOS 7 Gitlab 설치 및 설정(1) - Gitlab 설치하기1. External url 설정 변경설정 파일 위치 : /etc/gitlab/gitlab.rbsudo vim /etc/gitlab/gitlab.rb: 설정 파일 내에서 external_url 부분을 찾아서 원하는 url 로 변경2. 저장소 데이터 저장 위치 변경설정 파일 위치 : /etc/gitlab/gitlab.rbsudo vim /etc/gitlab/gitlab.rb: 설정 파일 내에서 git_data_dirs 경로 변경3. SMTP 설정설정 파일 위치 : /etc/gitlab/gitlab.rbsudo vim /etc/gitlab/gitlab.rb: SMTP 를 이용한 메일링 서비스 이용을 할 경우 파일 내의 SMTP 설정 부분 변경※Gitlab SMTP 설정 관련 공식 문서 : Gitlab SMTP 설정 관련 Docs4. Gitlab 변경된 설정 적용하기sudo gitlab-ctl stop\t\t\t\t// gitlab 중지sudo gitlab-ctl reconfigure\t\t\t// 설정 적용sudo gitlab-ctl restart\t\t\t\t// gitlab 재시작",
"url": "/gitlab-install-2"
}
,
"gitlab-install-1": {
"title": "CentOS 7 Gitlab 설치 및 설정(1) - Gitlab 설치하기",
"author": "Simplehan",
"category": "",
"content": "CentOS 7 Gitlab 설치 및 설정(1) - Gitlab 설치하기OS : CentOS 7VERSION : gitlab-ce (community edition - 무료버전)1. Gitlab 설치시 필요한 패키지 설치sudo yum install -y curl policycoreutils-python openssh-serversudo systemctl enable sshdsudo systemctl start sshdsudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --permanent --add-service=httpssudo systemctl reload firewalld2. 설치 전/후 의존성 패키지 설치여부 확인1. policycoreutils-pythonyum list installed | grep policycoreutils-python2. openssh-serveryum list installed | grep openssh-server3. openssh-server 구동 여부 확인systemctl status sshd4. 메일링 서비스를 위한 postfix 설치(필요시)sudo yum install postfixsudo systemctl enable postfixsudo systemctl start postfix3. Gitlab 설치하기1. Gitlab-ce Package repository 추가 curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash2. Gitlab 을 접속하기 위한 url 설정 후 gitlab-ce 설치 sudo EXTERNAL_URL=\"자신의 도메인이나 접속가능한 IP:접속할 Port번호\" yum install -y gitlab-ce ex) sudo EXTERNAL_URL=\"https://localhost:8080\" yum install -y gitlab-ce3. 설치 완료후 EXTERNAL_URL 에 설정된 주소로 접속 후 Gitlab 페이지 확인4. 초기 페이지 에서 root 의 패스워드를 새로 지정하기 위한 페이지가 나온다.3. Gitlab 명령어sudo gitlab-ctl stop\t\t\t\t// gitlab 중지sudo gitlab-ctl reconfigure\t\t\t// 설정 적용sudo gitlab-ctl restart\t\t\t\t// gitlab 재시작",
"url": "/gitlab-install-1"
}
,
"wordpress-use-installer": {
"title": "워드프레스(wordpress) 설치형 사용법",
"author": "Simplehan",
"category": "",
"content": "워드프레스 설치형 사용법WordPress 설치형 사용법 설치형 워드프레스를 쉽게 사용하기위한 플랫폼들이 많은데• WAMP• MAMP• LAMP• XAMPP전부 한국어를 지원하지 않으므로, 국내 개발된 autoset을 활용한 워드프레스 사용법입니다.• AutoSet운영체제 시스템에 따라 알맞게 다운로드 합니다.다른 아파치서버나 mysql 서버와 충돌이 날 수 있으므로 종료 후 설치를 진행합니다.설치가 완료 된 후 오토셋을 실행시 다음과 같은 화면이 나옵니다.기본으로는 웹 서버와 mysql 시작이라고 나올것인데 시작해주시면 됩니다.그후 제어탭을 눌러 phpMyAdmin에 접속합니다.초기화면에서 기본값으로 로그인 할 경우//기본값사용자명 : root패스워드 : autoset 다음과 같은 화면이 나오는데,해당화면에서 데이터베이스에 들어가 wordpress에서 사용될 DB를 생성합니다.그리고 워드프레스 최신버전을 설치해줍니다.워드프레스 다운로드압축파일을 오토셋의 public_html 폴더에 해제해줍니다.그 후 localhost/wordpress 에 접속하여 워드프레스 환경설정을 해줍니다.또한 설치형은 플러그인을 무료로 사용 가능합니다.이러한 식으로 플러그인을 찾아 추가해 사용 할 수 있습니다.",
"url": "/wordpress-use-installer"
}
,
"wordpress-use": {
"title": "워드프레스(wordpress) 사용법",
"author": "Simplehan",
"category": "",
"content": "워드프레스 사용법WordPress 사용법먼저 사이트에 접속하여 회원가입 후 사이트 생성을 통해기본적인 테마, 호스트 등을 설정 할 수 있습니다.과정의 이미지는 다음과 같습니다.이러한 과정을 거쳐 최종적으로 무료로 시작을 눌러 완료 할 수 있습니다. (유료 호스팅시 결제를 통해 완료도 가능합니다.)또한 워드프레스는 사이트 설정시 테마를 선택하여 좀 더 편리하게 사이트를 생성 할 수 있습니다.그 후, 추가적인 수정은 사용자 정의하기 탭에서 가능합니다.플러그인은 유료결제시에만 이용이 가능한 것으로 보입니다.추가적인 커스터마이징시 위젯에서 html태그를 생성해 임의로 추가하여 사용 할 수도 있습니다.그리고 사이트에서 사용되는 이미지의 관리는 미디어 라이브러리를 통해 가능합니다.",
"url": "/wordpress-use"
}
,
"mysql-auth": {
"title": "MySQL 사용자 추가, 권한부여",
"author": "Simplehan",
"category": "",
"content": "MySQL 권한MySQL 사용자 추가사용자 계정을 추가 하기 전에 현재 생성된 사용자 계정을 확인하기 위해서는use mysql;select host, user from user;쿼리를 통해서 가능합니다.그 후, 사용자를 추가하고자 한다면create user '사용자 ID';create user '사용자아이디'@localhost identified by '비밀번호'; //로컬호스트에서만 접속가능create user '사용자아이디'@'%' identified by '비밀번호'; //외부에서도 접속가능이런식으로 생성이 가능 합니다.MySQL 사용자 DB 권한 부여및 제거MySQL에서 사용자 계정의 권한을 추가하거나 제거할 때에는 GRANT와 REVOKE 명령으로 가능합니다.현재 사용자에게 허가된 권한을 보기 위해서는SHOW GRANTS 를 사용합니다.권한부여를 하면서 비밀번호 변경까지 하려면 IDENTIFIED BY ‘비밀번호’; 를 통해 가능합니다.이러한 방식으로 권한을 부여하는 쿼리는 다음과 같습니다.GRANT ALL PRIVILEGES ON DB명.테이블 TO '사용자아이디'@호스트 IDENTIFIED BY '비밀번호'; //모든 권한GRANT select, insert, update PRIVILEGES ON DB명.테이블 TO '사용자아이디'@호스트 IDENTIFIED BY '비밀번호'; //select, insert, update 권한DB명과 테이블에 *을 주면 모든 DB의 테이블에 전역권한을 부여 할 수 있으나 권장되지는 않습니다.비밀번호 변경을 원치 않는다면 IDENTIFIED BY ‘비밀번호’; 를 제거합니다.이러한 작업을 마친후 반영을 위해flush privileges;명령어를 통해 메모리에 반영하면 됩니다.반대로 권한을 제거하고자 한다면,revoke all on DB명.테이블명 from '사용자아이디'; 쿼리를 통해 가능합니다.",
"url": "/mysql-auth"
}
,
"react-native-ios": {
"title": "Mac 에서 React Native 개발 환경 세팅",
"author": "Simplehan",
"category": "",
"content": "Mac 에서 React Native 개발하기Homebrew 설치※ Homebrew링크Homebrew는 맥(Mac)에서 패키지를 설치하고 관리하는 맥(Mac) 패키지 관리자입니다.우선 아래 명령어를 통해 맥(Mac)에 Homebrew가 설치되었는지 확인해봅시다.brew --version# 만약 설치가 되었다면, 아래와 같이 버전을 확인 할 수 있습니다.homebrew x.x.xhomebrew/homebrew-core (git revision xxxx; last commit 2020-xx-xx)만약, 버전이 표시되지 않는다면 다음과 같이 명령어를 통해 Homebrew를 설치 합니다./usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"설치가 정상적으로 완료되었다면, 이전에 실행한 버전확인 명령어를 통해 버전을 확인할 수 있습니다.Nodejs 설치※ Nodejs링크React native는 Javascript이므로, Javascript의 런타임인 Nodejs가 필요합니다.이전에 설치한 Homebrew 명령어를 통해 Nodejs를 설치합시다.brew install nodeNodejs를 설치하게되면, 기본적으로 Nodejs의 패키지 매니저인 NPM(Node Package Manager)도 같이 설치됩니다.설치가 완료되었다면 다음의 명령어를 통해 버전을 확인 할 수 있습니다.node -versionvx.x.xnpm --versionx.x.xWatchman 설치※ Watchman링크Watchman은 특정 폴더나 파일을 감시하다가 변화가 생기면, 특정 동작을 실행하도록 설정하는 역할을 합니다.react-native에서는 소스코드의 추가, 변경이 발생하면 다시 빌드하기 위해 Watchman을 사용하고 있습니다.Watchman을 설치 하기 위하여 아래의 Homebrew 명령어를 통해 설치합니다.brew install watchman설치가 정상적으로 완료되었다면, 다음의 명령어를 통해 버전을 확인할 수 있습니다.watchman –versionReact Native CLI 설치※ React Native CLI링크react-native로 앱을 개발하기 위해 필요한 React Native CLI를 설치합시다.아래의 npm 명령어를 통해 React Native CLI를 설치 할 수 있습니다.npm install -g react-native-cli설치가 정상적으로 완료되었다면, 다음의 명령어를 통해 버전을 확인할 수 있습니다.react-native --versionXCode 설치※ Xcode다운로드 링크iOS 개발을 하기 위해서는 Xcode가 필요합니다.위의 다운로드 링크를 통해 Xcode를 다운로드 합니다.그 후 상단 메뉴에서 Xcode > Preferences… > Locations로 이동하여 아래와 같이 Command Line Tools가 잘 설정되었는지 확인합니다.만약 사진과 같이 설정되어 있지 않다면,가장 최신의 Command Line Tools을 선택해 줍시다.Cocoapods 설치※ Cocoapods링크Cocoapods는 iOS 개발에 사용되는 의존성 관리자입니다.react-native로 iOS 앱을 개발하려면 꼭 필요하므로 아래에 명령어를 사용하여 Cocoapods를 설치합니다.sudo gem install cocoapods설치가 완료되면, 다음 명령어를 통해 정상적으로 되었는지 확인합니다.pod --versionReact Native 프로젝트 생성 및 확인react-native는 버전에 따라 잘 동작하던 앱이 에러를 발생하는 등 문제를 일으킬 가능성이 높습니다.따라서 아래 npm 명령어를 통해 버전을 고정 시켜 사용하는 것을 추천합니다.npm config set save-exact=true이제 React Native CLI 명령어를 통해 프로젝트를 생성합니다# AppName 에 원하는 이름을 넣어 프로젝트를 생성 할 수 있습니다.react-native init AppName생성이 완료되면 아래의 명령어를 통해 구동 할 수 있습니다cd AppNamenpm run ios실행이 잘 되지 않는 경우, ios/AppName.xcworkspace 파일을 실행하고왼쪽 상단의 시뮬레이터를 설정하고 화살표 버튼을 눌러 시뮬레이터를 실행합니다.잘 실행이 되었다면, 아래와 같은 화면을 확인할 수 있습니다.",
"url": "/react-native-ios"
}
,
"react-native-preferences": {
"title": "window 에서 react-native 환경설정",
"author": "Simplehan",
"category": "",
"content": "react-native 환경설정JDK 8.0 다운받기※ node.js 사이트에서 다운로드 한다.http://www.oracle.com/technetwork/java/javase/downloads/index.htmlAndroid Studio 다운받기https://developer.android.com/studio/?hl=ko설치 후 실행하면 SDK를 자동으로 설치한다안드로이드 스튜디오를 실행하여, 에뮬레이터를 추가한다에뮬레이터를 사용할 수 없을 때는, 안드로이드 휴대폰을 pc와 연결한다node.js 다운받기※ node.js 사이트에서 다운로드 한다.https://nodejs.org/ko/download/설치가 완료되면 nodejs가 설치된 폴더를 환경변수에 추가해준다이후 프로그램 개발도구에서 명령어를 입력한다필자는 Visual Studio를 사용한다Visual Studio를 실행 후 터미널에서 react-native cli 설치 명령어를 입력한다명령어 : npm install -g react-native-cli설치가 완료되면 React-native 프로젝트를 생성한다명령어 : react-native run-android프로젝트 생성이 완료되면 프로젝트가 있는 폴더로 이동 후 프로젝트를 실행한다생성된 프로젝트의 App.js가 실행되는 것을 볼 수 있다",
"url": "/react-native-preferences"
}
,
"postgresql-install": {
"title": "PostgreSQL 설치",
"author": "Simplehan",
"category": "",
"content": "PostgreSQL 설치방법PostgreSQL 다운받기※ PostgreSQL 사이트에서 다운로드 한다.https://www.enterprisedb.com/download-postgresql-binaries다운이 완료되면 ‘postgresql.exe’ 파일을 실행시킨다.Next를 누르다보면 설치할 소프트웨어를 선택하는 창이 나온다‘pgAdmin’ 은 ‘postgresql’ 서버를 사용할 수 있도록 해주는 GUI 도구이다이곳에서 함께 깔아도되고 따로따로 깔아도 상관없다Next를 누르다보면 데이터베이스 관리자 비밀번호를 지정해야 한다기본값으로 데이터베이스 관리자의 이름은 ‘postgres’ 이다계속 Next를 누르면 설치가 완료된다설치가 완료되면 프로세스가 동작한다pgAdmin 다운받기※ pgadmin 사이트에서 다운로드 한다.http://pgadmin.org설치파일을 다운받아 실행시킨다설치가 모두 완료되면 위와 같은 페이지가 열린다‘postgresql’ 을 설치할 때 설정한 비밀번호를 입력한다‘pgAdmin4’ 의 특징은 자체적으로 웹 서버를 실행하고, 그 웹 서버를 브라우저로 접속해서 데이터베이스를 조작한다브라우저가 실행되면 ‘pgAdmin’ 웹프로그램을 실행하는 url을 호출하여 마치 홈페이지에 접속하는 것처럼 진행된다",
"url": "/postgresql-install"
}
,
"mysql-install": {
"title": "MySQL 설치",
"author": "Simplehan",
"category": "",
"content": "MySQL 설치방법MySQL 다운받기※ MySQL 사이트에서 다운로드 한다.http://www.mysql.com/DOWNLOADS 탭 하단부 MySQL Community (GPL) DownloadsMySQL Installer for Windows아래쪽 Download 눌러서 진행바로 설치를 진행한다MySQL 설치하기※ 설치 전 필요에 맞게 설정 후 설치를 진행한다.먼저 설치타입을 설정한다두번째 ‘Check Requirements’ 에서 MySQL 을 설치할 때 추가로 설치할 프로그램을 선택한다‘Execute’ 를 누르면 자동으로 설치한다‘Execute’ 을 누르면 위에서 설치한 프로그램들을 인스톨 한다‘Accounts and Roles’ 에서 ‘Root’ 계정의 비밀번호를 설정한다서비스 항목에 저장될 이름을 지정한다‘Execute’ 을 눌르면 서비스 등록 및 MySQL서버 시작 등 항목을 차례대로 진행한다‘Check’ 버튼을 눌러서 설치한 MySQL서버가 제대로 동작하는지 테스트 할 수 있다설치를 완료한다위 과정이 이상없이 성공적으로 진행 되었다면 자동으로 ‘Shell’ 과 ‘MySQL Workbench’ 가 실행된다MySQL 한글(UTF8) 설정하기데이터셋 설정 확인[1. MySQL Workbench 확인] ‘Navigator’ 목록에서 ‘Status and System Variables’ 를 선택한다 오른쪽탭(System Variables)을 선택하고 ‘character’ 를 검색한다위와 같이 실행하면 현재 설정값을 알 수 있다.만약 설정값이 utf8이 아니라면 ‘my.ini’ 파일을 수정해야 한다(‘my.ini’ 파일 위치는 위의 결과값 맨 마지막 행의 dir 위치를 참고)‘my.ini’ 파일 안에 위 내용을 넣어주고 ‘MySQL’ 을 재시작한다다시 workbench로 확인하면 utf8로 설정된 것을 볼 수 있다[2. MySQL 콘솔 확인]MySQL 콘솔창에 ‘status’명령어를 입력하여 ‘characterset’ 을 확인‘show variables like ‘char%’;’ 명령어를 실행시켜 ‘characterset’ 관련된 변수를 출력하여 확인이 경우에도 위와 같은 방법으로 my.ini 파일을 추가해주면 해결된다",
"url": "/mysql-install"
}
,
"hyperledger-fabric-setenv": {
"title": "하이퍼레저 패브릭 환경 구성",
"author": "Simplehan",
"category": "",
"content": "하이퍼레저 패브릭 환경 구성※ 환경 구성에 대한 정보는 하이퍼레저 패브릭 공식 docs 에서 발췌 하였으며 이번에 진행하는 버전은 v2.0.0 입니다.[구성환경] Oracle VM Ubuntu 18.04[필수 구성 요소] git curl docker Go language docker-compose node, npm※ root 계정으로 진행하지 않는것을 추천git 설치※ 설치관련 자세한 정보는 Git홈페이지 참조sudo apt-get install gitcurl 설치sudo apt-get install curldocker 설치도커 repository 설정 apt 패키지 업데이트 & HTTPS repository 사용을 위한 패키지 설치 $ sudo apt-get update$ sudo apt-get install \\\tapt-transport-https \\\tca-certificates \\\tcurl \\\tgnupg-agent \\\tsoftware-properties-common 도커 공식 GPG Key 추가 $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - stable repository 설정 $ sudo add-apt-repository \\ \"deb [arch=amd64] https://download.docker.com/linux/ubuntu \\ $(lsb_release -cs) \\ stable\" 도커 설치 $ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io현재 계정에 도커 권한 주기sudo usermod -aG docker $USERdocker-compose 설치$ sudo apt-get install docker-composePython 설치 유무 확인(파이썬 버전 확인)$ python -V※ docker-compose를 설치할때 python2.7 버전이 설치됨 docker-compose 설치 이후 파이썬 버전 확인시 확인이 안될경우 파이썬 설치$ sudo apt-get install python※ 도커 설치 후 우분투 계정 재 로그인 해야 계정에 도커 권한이 적용됩니다.※ 설치에 대한 자세한 사항은 도커홈페이지 참조Go Language 설치※ 설치관련 자세한 정보는 Go 홈페이지 참조Go 설치 파일 다운로드$ wget https://dl.google.com/go/go1.14.3.linux-amd64.tar.gz설치 파일 압축 해제$ sudo tar -C /usr/local -xzf go1.14.3.linux-amd64.tar.gzGo Workspace 용 폴더 설정(샘플소스 저장)$ mkdir go\t\t\t//OptionalGo 환경변수 설정$ echo \"export GOPATH=$PWD/go\" >> ~/.profile\t\t\t\t // Go Workspace 설정$ echo \"export GOROOT=/usr/local/go\" >> ~/.profile\t\t\t\t// Go binary 경로 설정$ echo \"export PATH=$PATH:$GOROOT/bin\" >> ~/.profile\t \t // Go 명령어 사용을 위해 PATH 설정$ source ~/.profile\t\t\t\t\t\t\t\t\t\t\t // 환경변수 설정 저장Node.js 설치※ 설치관련 자세한 정보는 Node 홈페이지 참조(운영체제에 맞게 설치)※ 데비안,우분투 Node 설치관련 자세한 정보는 Node Github페이지 참조sudo apt-get install nodejsnode 설치시 npm은 같이 설치됨npm build 를 위한 build-essential 설치sudo apt-get install -y build-essential지금까지 설치한 환경 확인git --versioncurl --versiondocker --versiondocker-compose --versiongo versionnode --versionnpm -V버전확인이 안될 시 우분투 재시작 또는 로그아웃 후 재로그인 후 다시 확인위 프로그램들이 정상적으로 설치 되었을 경우Hyperledger Fabric Samples, Binaries, Docker Image 다운로드 및 설치fabric 폴더 생성 (계정의 home 폴더 에서 진행)cdmkdir fabricfabric 설치 위에서 계정에 docker 관련 권한을 주지 않았을 경우 다운로드 후 bash 실행 과정에서 permission 오류가 날 수 있으니 꼭 권한을 주고 진행 할 것 해당 다운로드 진행 시 sample, binaries, docker image 가 동시에 받아진다생성한 fabric 폴더 내에서 진행한다cd fabric최신버전curl -sSL https://bit.ly/2ysbOFE | bash -s특정 버전 설치 시# curl -sSL https://bit.ly/2ysbOFE | bash -s -- <fabric버전> <fabric-ca버전> <thirdpatry 버전>curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.0.0 1.4.4 0.4.18다운로드 및 설치가 완료 되면 fabric-samples 폴더가 생성되고 그 안에 아래 그림과 같이 생성됨을 확인 한 뒤/bin 폴더 내에 해당 파일이 존재하는지 확인 configtxgen configtxlator cryptogen discover idemixgen orderer peer fabric-ca-client해당 바이너리를 사용하지 위해 환경 변수 설정echo \"export PATH=<path to fabric download location>/bin:$PATH\" >> ~/.profile위 과정이 에러 없이 모두 성공적으로 진행 되었다면 하이퍼 레저 패브릭을 사용하기 위한 환경은 구성 된 것입니다. 다음 포스트에서는 하이퍼레저 패브릭에서 제공하는 test-network 를 통해 네트워크를 실행 테스트를 진행해 보겠습니다.다음 포스트를 기다리기 힘드신 분들은 하이퍼레저 패브릭 공식 docs 를 보고 진행해 보세요.",
"url": "/hyperledger-fabric-setenv"
}
,
"google-map-react4": {
"title": "JavaScript 프린트 제어",
"author": "Simplehan",
"category": "",
"content": "1. JavaScript Printjavascript만을 사용하여 print기능을 제어하는 방법을 정리했습니다.먼저 현재창에서 Class가 printWrap인 HTML 엘리먼트를 A4 크기로 설정 후,프린트 하는 방법입니다. const _tempHTML = document.getElementsByClassName(\"printWrap\")[0].innerHTML; window.onbeforeprint = function(){ document.body.innerHTML = \"<div class='printWrap' style='width:21cm; height: 29.7cm;'>\" + _tempHTML +\"/div>\"; } window.onafterprint = function(){ //새로고침하여 페이지를 다시 출력합니다. this.window.location.reload(); } window.print();또는 새 창을 출력하여 해당 창의 내용을 print 할 수도 있습니다. const _tempHTML = document.getElementsByClassName(\"printWrap\")[0].innerHTML; const innerHTMLs = \"<div class='printWrap' style='width:21cm; height: 29.7cm;'>\" + _tempHTML + \"</div>\"; const popupWindow = window.open(\"\", \"_blank\", \"width=700,height=800\"); popupWindow.document.write(\"<!DOCTYPE html>\"+\t\t\t \"<html>\"+\t\t\t \"<head>\"+\t\t\t \t\"<style type='text/css'>\"+\t\t\t \t\t\".endline{page-break-before:always}\"+\t\t\t \t\"</style>\"+\t\t\t \t\"<style type='text/css' media='print'>\"+\t\t\t\t \t\t\"@page {\"+\t\t\t\t \t\t \"size: auto; /* auto is the initial value */\"+\t\t\t\t \t\t \"margin: 0px 100px 0px 100px; /* this affects the margin in the printer settings */\"+\t\t\t\t \t\t\"}\"+\t\t\t\t \t\"</style>\"+\t\t\t \"</head>\"+\t\t\t \"<body>\"+innerHTMLs+\"</body>\"+\t\t\t \"</html>\"); popupWindow.document.close();\tpopupWindow.focus(); //1초후 새 창 프린트 window.setTimeout(function () {\t \tpopupWindow.print();\t \tpopupWindow.close();\t}, 1000);위와 같이 css media 에 print 속성을 주면 프린트 시 css를 설정 할 수 있습니다.해당 css에서는 margin의 top, bottom을 0으로 주어 상단 날짜와하단의 페이지 꼬리말을 제거후 가운데로 정렬한 예제입니다.이러한 방법을 통해 새창 또는 현재창에서 a4용지에 맞게javascript로 제어하여, 프린트 할 수 있습니다.",
"url": "/google-map-react4"
}
,
"openjdk-cacerts": {
"title": "Open JDK 사용 시 Https 접속 시 에러 해결",
"author": "Simplehan",
"category": "",
"content": "Oracle에서 JDK 유료화를 진행하려 함에 따라 open jdk 의 활용이 높아지고 있다.Open JDK 를 통해 Https 통신 요청을 하게 되면javax.net.ssl.SSLException: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty에러가 발생할 때가 있다 .Truststore라는 인증서들을 보관하는 파일(JRE/lib/security/cacerts)이 있는데, 파일 내부에 해당 https 요청을 할 때 사용할 인증서가 들어있지 않거나 혹은 이 인증서가 유효하지 않아서 나는 에러이 에러는 특히 OpenJDK를 사용할 때 발생하며 Sun, Oracle, Apple의 JDK를 사용하면 발생하지 않는다.해결 법은 open jdk의 cacerts 파일을 oracle jdk 의 cacerts 파일로 심볼릭 링크를 걸어 주거나. 파일을 교체해 주면 된다.이 포스트에서는 파일 교체를 통해 문제를 해결하는 법을 소개한다.Openjdk 경로/jre/lib/security 경로의 폴더를 보게 되면 cacerts 라는 파일이 있다. 리눅스 명령어 ls-l 을 통해 내용을 확인해 보면※현재 cacerts 교체를 한 상태로 기존 cacerts 파일은 cacerts_bak 이다.파일 용량이 32 바이트로 거의 비어 있는 파일이 존재 할 것이다. 이 파일을 기존 Oracle JDK 파일로 교체를 해주어야 한다.Oracle JDK 홈 디렉토리/jre/lib/security 폴더 내부에도 같은 파일(cacerts) 가 있을 것이다.이 파일을 Open JDK 내부의 cacerts 파일 대신에 사용하면 된다.나중을 대비해 Open JDK 의 cacerts 파일을 백업해 두고 작업을 하도록 하자※ 리눅스 파일 복사 명령어 : cp ‘복사할 파일경로(상대/절대)’ ‘복사한 파일을 붙여넣을 경로/붙여넣을 때 파일 명(상대/절대)’cp .../jdk180.../jre/lib/security/cacerts .../java-se-8u40-ri/jre/lib/security/cacerts파일 변경 뒤에 톰캣을 재시작 한뒤 테스트해보면 된다.",
"url": "/openjdk-cacerts"
}
,
"react-native-base": {
"title": "React Native Base",
"author": "Simplehan",
"category": "",
"content": "1. React native 프로젝트에 NativeBase 적용하기기본 사용 조건 Visual Studio Code Android Studio node.js프로젝트를 만들 때는 expo를 사용하는 방법, native-cli를 사용하는 방법 2가지가 있다.이번 글에서는 native-cli를 사용한 방법을 설명한다.react-native cli 설치npm install -g react-native-cli프로젝트 생성react-native init project_name프로젝트 동작cd project_name //생성한 프로젝트 폴더로 이동react-native run-ios // ios 일 때react-native run-android // android 일 때설치가 완료되면 위와 같은 메시지가 표시된다.NativeBase 사용import React, { Component } from 'react';import { Container, Header, Content, Button, Text } from 'native-base';export default class ButtonExample extends Component { render() { return ( <Container> <Header /> <Content> <Button> <Text>Click Me!</Text> </Button> </Content> </Container> ); }}기본 사용방식은 <Container> 안에 <Header>, <Content> 를 넣어 사용한다자세한 사용방법은 아래 공식 홈페이지에 설명되어 있다.https://docs.nativebase.io/Components.html#button-def-headref개발환경 설정에 유용한 라이브러리 React-Native 프로젝트 공식 사이트 NativeBase NativeBase UI 프레임워크 공식 사이트 reactnavigation react-native 페이지이동 라이브러리 vector-icons 무료지원 아이콘 라이브러리",
"url": "/react-native-base"
}
,
"openbank-api-2": {
"title": "오픈뱅킹 서비스 API 통신 - 2 (사용자 인증)",
"author": "Simplehan",
"category": "",
"content": "사용자 인증 이전 포스트에서 간편 가입 이후 등록한 앱에서 확인한 앱키와 앱 시크릿 키를 통한 사용자 인증을 진행해 보도록 하겠다. 사용자에 대한 이용기관의 자체 인증이 없다면 오픈뱅킹에서 제공하는 본인인증 절차를 통해 본인 확인 및 계좌 등록이 가능하다. OAuth2 기반의 인증 절차로 이루어 지며 자체 인증이 없는 경우 3-legged 방식을 통해 오픈뱅킹에서 제공하는 화면을 통해 인증을 진행한다. REST API 를 통해 진행할 수 없으며 웹 또는 앱 어플리케이션에서 인증을 위한 주소를 호출하여 인증화면을 제공 받는다. 이번 테스트에서는 스프링부트 웹 프로젝트를 통해 사용자 인증을 받는 방식을 설명하도록 하겠다. 간단히 테스트를 위한 스프링 부트 웹 프로젝트를 생성한 뒤 JSP 내에서 윈도우 팝업 호출을 통한 인증 페이지를 호출한다. 오픈뱅킹에서 제공하는 API 명세서 중 본인인증 요청 API 확인URL : https://openapi.openbanking.or.kr/oauth/2.0/authorizeMethod : GET의 주소를 통한 JSP 내의 form 태그를 이용한 윈도우 팝업 호출form 코드<form id=\"authorizeFrm\" name=\"authorizeFrm\" method=\"get\" action=\"https://testapi.openbanking.or.kr/oauth/2.0/authorize\" target=\"_authForm\"> <input type=\"hidden\" name=\"client_id\" value=\"AyEoyDUkvMZznIdzL1pGdFxTL1rQR2q2OwdJYQMv\"/> <input type=\"hidden\" name=\"scope\" value=\"login inquiry transfer\"/> <input type=\"hidden\" name=\"redirect_uri\" value=\"http://localhost:8082/bank/callback\"/> <input type=\"hidden\" name=\"auth_type\" value=\"0\"/> <input type=\"hidden\" name=\"response_type\" value=\"code\"/> <input type=\"hidden\" name=\"state\" value=\"12345678901234567890123456789012\"/></form>client_id : 앱 등록 후 발급받은 앱 키scope : Access Token 권한 범위 (다중 scope 가능)redirect_uri : 마이페이지 > 내앱관리 > 추가정보 에서 사용자 인증 이후 redirect 될 uri 를 설정 후 해당 주소 사용auth_type :(0)최초인증 : 처음 오픈뱅킹에 접속하는 사용자의 인증 처리 (본인인증+조회/출금동의)(1)재인증 : 최초 접속 이후에 추가로 계좌등록 필요시 인증 처리 (본인재인증+ARS동의) 선택항목을 파라미터로 받은 경우 인증화면 입력란에 자동으로 정보 셋팅(2)인증생략 : 유효한 접속토큰을 이용하여 본인인증 절차 생략 (본인인증생략+조회/출금동의) ARS로 조회/출금동의 시 휴대폰번호는 DB에 저장된 값을 활용 (전자금융업자 등 특정 자격요건을 갖춘 사업자에만 선별적으로 허용)response_type : “code” 로 고정state : CSRF 보안위협에 대응하기 위해 이용기관이 셋팅하는 난수값(32Byte 로 고정)※ 각 파라미터에 대한 자세한 설명은 오픈뱅킹 서비스 API 목록 (Ctrl + click 시 새탭으로 열기) 의 사용자 인증 에서 확인위의 form 코드를 window.open 함수를 이용한 새창에서 호출 스크립트 코드Javascript 코드var $authorizeFrm = $('#authorizeFrm');window.open('', '_authForm');$authorizeFrm.submit();새창에서 해당 정보를 가지고 호출을 하면 다음과 같은 화면을 볼 수 있다.이후 각 인증절차에 따라 인증을 진행하면 파라미터로 설정한 redirect_uri 를 통해 Access Token을 발급 받기 위한 code(authorization_code) 를 리턴 받을 수 있다.※응답 형식redirect_uri 로 설정된 경로에서 code 값을 추출**사용자 토큰 발급 ** (3-legged)****추출한 code 값을 이용하여 토큰 발급 API 요청을 진행한다.****HashMap<String, String> authHeaders = new HashMap<>(); authHeaders.put(\"content-type\", \"application/x-www-form-urlencoded; charset=UTF-8\"); HashMap<String, Object> authParams = new HashMap<>(); authParams.put(\"code\", reqParams.get(\"code\")); authParams.put(\"client_id\", clientId); authParams.put(\"client_secret\", clientSecret); authParams.put(\"grant_type\", \"authorization_code\"); authParams.put(\"redirect_uri\", redirectUri); JSONObject authRes = HttpUtils.httpPost(\"https://testapi.openbanking.or.kr/oauth/2.0/token\", authHeaders, authParams); String accessToken = (String) authRes.get(\"access_token\"); String refreshToken = (String) authRes.get(\"refresh_token\"); String userSeqNo = (String) authRes.get(\"user_seq_no\"); String scope = (String) authRes.get(\"scope\"); String tokenType = (String) authRes.get(\"token_type\");",
"url": "/openbank-api-2"
}
,
"openbank-api-1": {
"title": "오픈뱅킹 서비스 API 통신 - 1",
"author": "Simplehan",
"category": "",
"content": "계정 생성(테스트를 위한 간편가입)오픈 뱅킹에서는 테스트 이용자를 위한 테스트베드 서비스를 제공한다.테스트 베드 웹 사이트 https://developers.openbanking.or.kr/ 접속상단 메뉴 우측 간편가입을 통해 간편가입 진행앱 등록 및 앱 키 발급로그인 후 우측 마이페이지 > 내앱 선택 > 내앱 관리에서 앱 등록 후 앱키와 앱 시크릿 키를 발급 받을 수 있다.여기서 발급 받은 앱키와 앱 시크릿 키를 이용하여 사용자 인증시에 요청 파라미터에 사용테스트 베드 API 콘솔테스트 베드에서 제공하는 API 콘솔을 이용하여 각 API 별로 요청 파라미터와 응답 데이터에 대한 간단한 API 테스트가 가능하다.API 의 구조 파악과 통신에 대한 간단한 테스트만 가능 할 뿐 테스트 베드에서 제공하는 테스트 데이터와는 무관한 데이터가 나온다.다음 포스트에서 테스트 베드를 통해 제공받은 앱키와 앱 시크릿 키를 이용해 사용자 인증을 진행해보도록 하겠다.",
"url": "/openbank-api-1"
}
,
"javascript-print": {
"title": "JavaScript 프린트 제어",
"author": "Simplehan",
"category": "",
"content": "1. JavaScript Printjavascript만을 사용하여 print기능을 제어하는 방법을 정리했습니다.먼저 현재창에서 Class가 printWrap인 HTML 엘리먼트를 A4 크기로 설정 후,프린트 하는 방법입니다. const _tempHTML = document.getElementsByClassName(\"printWrap\")[0].innerHTML; window.onbeforeprint = function(){ document.body.innerHTML = \"<div class='printWrap' style='width:21cm; height: 29.7cm;'>\" + _tempHTML +\"/div>\"; } window.onafterprint = function(){ //새로고침하여 페이지를 다시 출력합니다. this.window.location.reload(); } window.print();또는 새 창을 출력하여 해당 창의 내용을 print 할 수도 있습니다. const _tempHTML = document.getElementsByClassName(\"printWrap\")[0].innerHTML; const innerHTMLs = \"<div class='printWrap' style='width:21cm; height: 29.7cm;'>\" + _tempHTML + \"</div>\"; const popupWindow = window.open(\"\", \"_blank\", \"width=700,height=800\"); popupWindow.document.write(\"<!DOCTYPE html>\"+\t\t\t \"<html>\"+\t\t\t \"<head>\"+\t\t\t \t\"<style type='text/css'>\"+\t\t\t \t\t\".endline{page-break-before:always}\"+\t\t\t \t\"</style>\"+\t\t\t \t\"<style type='text/css' media='print'>\"+\t\t\t\t \t\t\"@page {\"+\t\t\t\t \t\t \"size: auto; /* auto is the initial value */\"+\t\t\t\t \t\t \"margin: 0px 100px 0px 100px; /* this affects the margin in the printer settings */\"+\t\t\t\t \t\t\"}\"+\t\t\t\t \t\"</style>\"+\t\t\t \"</head>\"+\t\t\t \"<body>\"+innerHTMLs+\"</body>\"+\t\t\t \"</html>\"); popupWindow.document.close();\tpopupWindow.focus(); //1초후 새 창 프린트 window.setTimeout(function () {\t \tpopupWindow.print();\t \tpopupWindow.close();\t}, 1000);위와 같이 css media 에 print 속성을 주면 프린트 시 css를 설정 할 수 있습니다.해당 css에서는 margin의 top, bottom을 0으로 주어 상단 날짜와하단의 페이지 꼬리말을 제거후 가운데로 정렬한 예제입니다.이러한 방법을 통해 새창 또는 현재창에서 a4용지에 맞게javascript로 제어하여, 프린트 할 수 있습니다.",
"url": "/javascript-print"
}
,
"jasper-2": {
"title": "JasperReports Java Spring project 연동 사용법",
"author": "Simplehan",
"category": "",
"content": "1. JasperStudio Java Spring Project 연동 사용법먼저 spring mvc project를 생성합니다.그 후 연동에 필요한 pom.xml의 dependency를 추가해줍니다(예제에서는 기존 jrxml을 만들때와 같이 mysql연동을 위해 디펜던시를 추가했습니다.) <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.31</version> </dependency> <!-- jasper --> <dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.11.0</version> </dependency> <dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports-functions</artifactId> <version>6.11.0</version> </dependency> <dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports-fonts</artifactId> <version>6.11.0</version> </dependency>컨트롤러에서 먼저 connection을 생성후, 기존에 만든 jrxml과 연동하는 코드입니다.\t@RequestMapping(value = \"/\", method = RequestMethod.GET)\tpublic String home(Model model, HttpServletRequest request) { String folderPath = \"c:\\\\test\\\\\";\t\tConnection conn = null;\t\ttry {\t\t\tClass.forName(\"com.mysql.jdbc.Driver\");\t\t\tconn = DriverManager.getConnection(\"jdbc:mysql://U R L/T A B L E\", \"U S E R\", \"P A S S W O R D\");\t\t\tJasperReport jasperReport = JasperCompileManager.compileReport(folderPath+\"simpleTest.jrxml\");\t\t\tJasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, null, conn);\t\t\tjasperReport.setWhenNoDataType(WhenNoDataTypeEnum.ALL_SECTIONS_NO_DETAIL); /* html 파일로 변환합니다, JasperViewer.viewReport(jasperPrint, false); 를 통해 자체적으로 출력할 수 도있습니다. 대신 client가아닌 server에서만 jasper reports화면이 출력됩니다.. */\t\t\tJasperExportManager.exportReportToHtmlFile(jasperPrint, folderPath+\"simpleTest.html\");\t\t\tconn.close();\t\t} catch (Exception e) {\t\t\te.printStackTrace();\t\t}\t\t model.addAttribute(\"folderPath\", folderPath);\t\treturn \"home\";\t}그 후, JSP 파일에서 생성한 HTML파일을 로드하여 삽입하여 사용하면 끝입니다.<div>\t<button onclick=\"onPrint()\">프린트</button>\t\t<div id=\"bodyWrap\"></div>\t<!-- #bodyWrap tab -->\t\t<div id=\"printWrap\" style=\"display: none; height: 0\">\t</div></div><script type=\"text/javascript\"> var folderPath = \"${folderPath}\"; $(function () {\t\t$(\"#bodyWrap\").load(\"resources/simpleTest.html\");\t\twindow.setTimeout(function () {\t\t\tvar html = $(\"#bodyWrap\").children(\"table\").children(\"tbody\").children(\"tr\").children(\"td\")[1];\t\t\tvar imgs = $(html).find(\"img\");\t\t\tfor (let i=0; i<imgs.length; i++) { /* jasper(jrxml) -> html 변환시 사용된 이미지는 해당 파일의 이름으로된 폴더를 생성 후, 상대경로로 img태그의 src값으로 사용되어 있습니다. 이 값을 절대 경로값으로 잡아 불러와줍니다. */\t\t\t\timgs[i].src = folderPath + imgs[i].src;\t\t\t};\t }, 1000);\t});</script>onPrint 같은경우엔 javascript 프린트 제어 사용법을게시해두었으니 참고하여 작성하면 되겠습니다.",
"url": "/jasper-2"
}
,
"jasper-1": {
"title": "JasperReports 사용법",
"author": "Simplehan",
"category": "",
"content": "1. JasperStudio 가이드JasperStudioNew > Jasper Report 를눌러 jrxml 파일을 생성합니다. 리포트 템플릿을 통해 기본 레이아웃 생성 가능 합니다. 그 후 DB 설정 등이 가능한데, 작업 후에도 가능하므로 Finish 합니다. 그리고 작업화면에서드래그앤 드롭을 이용해 텍스트필드등을 레이아웃에 삽입하거나,source 탭을 눌러 코드로 작성도 가능합니다. 그 후 DB 연동을 위해 Data Adapters에서 우클릭 후 Create Data Adapter를 눌러 원하는 데이터(현재 예제에서는 MySQL로 진행) 선택 가능 합니다. Database JDBC Connection탭을 선택 후 Next JDBC Driver에서 MySQL을 선택, DB 정보 입력 후 Test 버튼을 눌러, 테스트하여 다음과 같은 창이 나오면 성공입니다. 실패 시 Driver Classpath에 maven mysql files를 추가합니다. SQL문을 통해 Outline 탭의 Parameters에서사용 할 수 있는 Data paramers를 설정 할 수 있습니다. 그 후 드래그 앤 드롭 또는 텍스트필드를 생성 후다음과 같이 입력하여 data를 사용 할 수 있습니다. 이런 방식으로 간단히 보여줄 페이지를 작성 합니다. Preview를 누르면 작업 완료 내용을 미리 볼 수 있습니다. 작업이 끝났다면, jrxml파일을 jasper로 컴파일할 수도 있습니다. ",
"url": "/jasper-1"
}
,
"android-project-structure": {
"title": "Android 프로젝트 구조",
"author": "Simplehan",
"category": "",
"content": "1. Android 프로젝트 구조안트로이드 어플리케이션 프로젝트의 구조는 크게 4가지로 나눌 수 있다. 1. manifests - 'AndroidManifest.xml' 안드로이드의 컨트롤 타워라고 할 수 있다. 어플리케이션을 구동하는데 필요한 설정값을 관리해주는 곳이다.2. java - 클래스를 관리하는 폴더이다.3. res - Resource 폴더로 UI와 관련된 파일과 디자인 리소스, 문자열 리소스를 담고 있는 폴더이다.4. bulid.gradle - 어플리케이션을 빌드하기위해 필요한 설정 옵션, 라이브러리 정보들이 들어있다.a. AndroidManifest.xml 어플리케이션이 구동하는데 필요한 요소를 ‘AndroidManifest’에서 추가, 설정하고 어플리케이션과 장치간에 상호작용하기 위해 필요한 권한 등을 설정한다.이곳에서 패키지의 이름을 정의하거나, 어플리케이션의 이름, 화면에 표시될 아이콘 등을 설정할 수 있다. 또한 어플리케이션에서 사용할 구성요소들의 클래스명을 지정하고 각 클래스들이 실행될 수 있는 조건을 기술한다.b. java 패키지 ‘안드로이드를 프로그래밍한다.’ 라고 말하는 부분은 이곳을 수정한다는 얘기와 다르지 않다. 어플리케이션이 작동하는 방식에 대해 정의하는 java 파일은 모두 이곳에 작성한다.setContentView(R.layout.activity_main); 의 코드가 해당 java 파일을 실행할 화면을 지정하는 것인데, 때문에 기본적으로 ‘Activity’ 파일 하나와 ‘xml’ 파일이 한쌍이 된다.c. activity_main.xml 연결된 Activity 에서 작성한 코드가 어떻게 보여지는지 이곳에서 수정한다. 이곳이 사용자가 보게 될 UI 화면이다.안드로이드 프로젝트 구조면에서 볼 때 기억해야 할 것은 ‘Application’ 위에 ‘Activity’ 가 실행된다는 것과 기능은 ‘Activity’ 에서 UI는 ‘xml’ 에서 작성한다는 것, 그리고 ‘Activity’ 마다 ‘xml’ 파일이 연결되어 있다는 것이다. 따라서 새로운 화면을 만들때 ‘xxx_Activity.java’ 파일을 만들고 그 파일에 activity_xxx.xml 파일을 연결해 주면 되는 것이다.2. Android Activity 생명주기(LifeCycle)Activity 생명주기란?어플리케이션에서 사용자가 ‘Activity’를 동작시키고 다음 ‘Activity’로 이동할 때까지 이전 ‘Activity’의 처음 생성부터 종료될 때까지 어떻게 동작하는지를 말한다.위 이미지를 순서대로 설명하면 아래와 같다. onCreate - 맨처음 사용자가 액티비티를 동작시키면 onCreate() 메소드가 실행되며 액티비티를 생성한다. onStart() - 생성을 마치면 메소드가 실행되며 액티비티가 화면에 보여지기 전 준비를 하는 단계이다. onResume() - 액티비티 동작 준비가 완료된 후 실행된다. 액티비티가 동작된다. onPause() - 사용자가 다음 액티비티로 이동하거나, 현재 액티비티가 휴대폰 화면에서 다른화면에 가려지거나, 보이지 않을때 실행된다. onStop() - 화면이 다른 액티비티로 완전히 이동했거나, 다른 어플리케이션이 동작하여 화면에서 완전히 사라진 경우 실행된다. onDestroy() - 액티비티가 종료되거나, 시스템에서 어플리케이션이 삭제되었을 때 액티비티를 파괴한다. onRestart() - onStop() 상태에서 사용자가 해당 액티비티로 돌아오거나, 재시작할 때 다시 onStart()로 돌아가 화면에 보여질 준비를 한다.이렇게 여러 단계를 거치며 액티비티간 이동이 일어난다. 단계별로 호출되는 메서드가 있기 때문에 프로그래밍을 하는데 필요한 데이터를 효율적으로 운용할 수 있다. 예를들어 onPause() 메소드는 데이터를 저장하는데 주로 사용하는데, 데이터를 입력중에 액티비티 화면이 전환되는경우 데이터를 저장하고 있다가 해당 액티비티가 재시작되면 그대로 이어서 입력할 수 있도록 할 때, 또는 사용자가 해당 액티비티로 되돌아올때 가져와야하는 데이터가 있는경우 OnPause() 에서 처리할 수 있다.액티비티의 생명주기는 안드로이드 프로그래밍을 함에 있어서 액티비티의 생성부터 종료까지의 과정에서 상황에 따라 호출되는 메서드가 다르기 때문에 꼭 이해하고 있어야 한다.",
"url": "/android-project-structure"
}
,
"react-back-end": {
"title": "React back-end",
"author": "Simplehan",
"category": "",
"content": "1. Koa 로 웹서버 만들기a. Koa 란?Node.js에서 가장 인기있던 웹 프레임워크인 Express.js 개발팀이 Koa 라는 웹프레임워크를 새로 만들었다.Express 와의 차이점은 Koa가 훨씬 가볍고, Node.js v7 의 async/await 기능을 아주 편하게 사용할 수 있다는 것이다.async/await 를 자체적으로 사용할 수 있기 때문에 Koa 는 Node.js v7 이상 버전에서 사용하는 것이 권장된다.b. Koa 기본 사용법-서버를 여는 방법-src/index.jsconst Koa = require('koa');const app = new Koa();app.use(ctx => { ctx.body = 'Hello Koa';});app.listen(4000, () => { console.log('heurm server is listening to port 4000');});위 코드를 작성하여 실행시킨 후 브라우저에서 http://localhost:4000/ 에 들어가면, Hello Koa 라는 텍스트가 보인다.-async/await 사용하기-const Koa = require('koa');const app = new Koa();app.use(async (ctx, next) => { console.log(1); const started = new Date(); await next(); console.log(new Date() - started + 'ms');});app.listen(4000, () => { console.log('heurm server is listening to port 4000');});app.use 라는 함수는 미들웨어다. Koa의 미들웨어 함수에서는 두가지 파라미터를 받는다.ctx는 웹요청과 응답에 대한 정보를 가지고 있고, next 는 다음 미들웨어를 실행시키는 함수다.next를 호출하지 않는다면 그 부분에서 요청처리를 완료시키고 응답을 하게 된다.async/await 문법을 사용하면 콜백이 여러개 겹치는 문제가 사라진다.이것은 서버에서도 자주 사용하게 되는데 이는 데이터베이스에 요청을 할 때 매우 유용하게 사용된다.-REST API 사용하기-REST API 는 요청의 종류에 따라 다른 HTTP 메소드를 사용한다. GET: 데이터를 가져올 때 사용한다. POST: 데이터를 등록 할 때 사용한다. 혹은, 인증작업을 거칠때도 사용된다. DELETE: 데이터를 지울 때 사용된다. PUT: 데이터를 교체 할 때 사용된다. PATCH: 데이터의 특정 필드를 수정 할 때 사용된다.라우터를 사용하여 각 메소드에 대한 요청을 준비해보자.src/api/rest_api/rest_api.controller.jsexports.list = (ctx) => { ctx.body = 'listed';};exports.create = (ctx) => { ctx.body = 'created';};exports.delete = (ctx) => { ctx.body = 'deleted';};exports.replace = (ctx) => { ctx.body = 'replaced';};exports.update = (ctx) => { ctx.body = 'updated';};src/rest_api/index.jsconst Router = require('koa-router');const rest_api = new Router();const rest_apiCtrl = require('./rest_api.controller');rest_api.get('/', rest_apiCtrl.list);rest_api.post('/', rest_apiCtrl.create);rest_api.delete('/', rest_apiCtrl.delete);rest_api.put('/', rest_apiCtrl.replace);rest_api.patch('/', rest_apiCtrl.update);module.exports = rest_api;코드가 복잡해지면 모듈화하여 파일을 분리시키는 것이 좋다.분리시킬수록 코드의 가독성이 높아지고 유지보수 하기에도 편해진다.2. DB 연동하기서버 폴더에서 connection 파일를 만들어 각 항목의 값을 자신의 DB 설정에 맞게 입력한다.먼저 사용할 DB와 dotenv를 인스톨 해야한다.프로젝트의 루트경로에 .env 파일을 만들고 mongoDB의 웹서버의 포트번호와 DB주소를 넣는다.보안을 위해 따로 파일을 만들어서(.env) DB의 정보를 환경변수로 빼내는 것이다.dotenv가 여기서 환경변수를 파일안에 넣어서 불러올 수 있도록 해준다.a. MySQL과 연동하는 방법.var mysql = require('mysql');var dbConfig = { host: 'localhost', user: 'root', password: '1234', port: 3306, database: 'nelp' connectionLimit : 50};var pool = mysql.createPool(dbConfig);// Get Connection in Poolpool.getConnection(function(err,connection){ if(!err){ //connected! } // 커넥션을 풀에 반환 connection.release();}); 1. 다수의 Connection 관리 기법 2. Pool에서 Connection 얻어서 사용하고 Pool에 반납 3. waitForConnections : 풀에 여유 커넥션이 없는 경우 대기 여부 4. connectionLimit : 최대 커넥션 개수, 기본 10개b. Mongoose를 사용하여 MongoDB 연동하는 방법..envPORT=4000MONGO_URI=mongodb://localhost/project_name다음은 src/index.js 파일 상단에 dotenv를 적용한다.src/index.jsrequire('dotenv').config(); // .env 파일에서 환경변수 불러오기const Koa = require('koa');const Router = require('koa-router');const app = new Koa();const router = new Router();const api = require('./api');const mongoose = require('mongoose');mongoose.Promise = global.Promise; // Node 의 네이티브 Promise 사용// mongodb 연결mongoose.connect(process.env.MONGO_URI, { useMongoClient: true}).then( (response) => { console.log('Successfully connected to mongodb'); }).catch(e => { console.error(e);});const port = process.env.PORT || 4000; // PORT 값이 설정되어있지 않다면 4000 을 사용합니다.router.use('/api', api.routes()); // api 라우트를 /api 경로 하위 라우트로 설정app.use(router.routes()).use(router.allowedMethods());app.listen(port, () => { console.log('heurm server is listening to port ' + port);});DB가 연결되면 스키마를 작성해야 한다.간단하게 말해서 해당 컬렉션에 어떤 항목이 들어가는지, 각 데이터의 형식이 무엇인지어떤 종류의 값이 들어가는지 정의하는 것이다.간단한 예로 books에 대한 데이터 스키마를 준비하고 모델을 만들어보자.책의 정보를 담는 데이터는 어떤 값들이 필요한가? 책 이름 저자 출판일 가격 …각 항목들이 어떤 형식으로 이루어져야 하는지 생각해보자. 책 이름: 문자열 저자: 저자의 정보(저자명, email, 저자의 수 등) 출판일: 날짜 가격: 숫자 …위의 데이터로 스키마를 만들어보자.const mongoose = require('mongoose');const { Schema } = mongoose;// Book 에서 사용할 서브다큐먼트의 스키마입니다.const Author = new Schema({ name: String, email: String});const Book = new Schema({ title: String, authors: [Author], // 위에서 만든 Author 스키마를 가진 객체들의 배열형태로 설정했습니다. publishedDate: Date, price: Number, tags: [String], createdAt: { // 기본값을 설정할땐 이렇게 객체로 설정해줍니다 type: Date, default: Date.now // 기본값은 현재 날짜로 지정합니다. }});// 스키마를 모델로 변환하여, 내보내기 합니다.module.exports = mongoose.model('Book', Book);위의 코드에서 Author와 Book이 스키마이고, 하단에 mongoose.model 을 통하여만든 스키마를 모델로 변환하여 다른 파일에서 불러와 사용 할 수 있도록 내보낸 것이다.MongoDB가 설치되어 있다면, mongo 명령어를 통하여 데이터베이스에 접속해 데이터를 조회할 수 있을 것이다.3. SQL과 NoSQLa. SQL 이란?SQL은 ‘관계형 데이터베이스’ 또는 ‘구조화 된 쿼리 언어(Structured Query Language)’의약자로 특정 유형의 데이터베이스와 상호 작용하는데 사용하는 쿼리 언어라고 한다.SQL의 데이터는 정해진 데이터 스키마를 따라 데이터베이스 테이블에 저장된다.각 테이블에는 명확하게 정의된 구조가 있다. 따라서 스키마를 준수하지 않는 레코드는 추가할 수 없다.명확하게 정의된 구조로 데이터의 무결성을 보장하고, 관계는 각 데이터를 중복없이 한번만 저장한다.SQL을 사용하면 관계형 데이터베이스 관리 시스템(RDBMS)에서 데이터를 저장, 수정, 삭제 및 검색 할 수 있다.b. NoSQL 이란?기본적으로 SQL과 반대되는 접근방식을 따르기 때문에 지어진 이름이다.스키마와 데이터간의 관계가 없다. NoSQL 에서는 레코드를 문서(documents)라고 부른다.핵심적인 차이는 SQL에서는 정해진 스키마를 따르지 않는다면 데이터를 추가할 수 없지만,NoSQL에서는 다른 구조의 데이터를 같은 컬렉션(=SQL에서 테이블)에 추가할 수 있다.위에서 스키마를 따로 만들어주어야 하는것은 MongDB가 NoSQL이기 때문이다.NoSQL의 문서는 JSWON데이터와 비슷한 형태를 가지고 있다. 또한 일반적으로 관련 데이터를동일한 컬렉션에 모두 담고 있다.이런 방식은 데이터가 중복되기 때문에 불안정하다는 단점이 있다.예를들면, 실수로 B컬렉션에서는 데이터를 수정하지 않았는데, A컬렉션에서만 데이터를 업데이트 하는 경우이다.그럼에도 불구하고, 이 방식의 가장 큰 장점은 복잡한 조인을 사용할 필요가 없다는 것이다.이미 하나의 컬렉션에 모든 데이터가 저장되어 있기 때문이다.특히 자주 변경되지 않는 데이터 일때 더 큰 장점이 된다.어떤 데이터베이스를 쓸 것이냐는 어떠한 데이터를 다루는지, 어떤 애플리케이션에서 사용되는지 고려해야 한다.",
"url": "/react-back-end"
}
,
"router-codesplitting-serverrendering": {
"title": "React Router, Code Splitting, Server Side Rendering",
"author": "Simplehan",
"category": "",
"content": "1. React-routera. React-router 란?공식은 아니지만 가장 많이 사용되고 있는 라이브러리이다.이 라이브러리는 클라이언트 사이드에서 이루어지는 라우팅을 간단하게 해주며,화면이 여러개인 웹 어플리케이션을 만든다면 React-router는 필수 라이브러리이다.전통적인 웹어플리케이션의 구조는, 여러 페이지로 구성되어 있다.유저가 요청할 때마다 페이지가 새로고침되며, 페이지를 로딩 할 때마다 서버로부터리소스를 전달받아 해석 후 렌더링을 한다.요즘은 웹에서 제공되는 정보가 정말 많기 때문에 속도적인 측면에서 문제가 있었고,이를 해소하기 위해 SPA(Single Page Application) 방식이 생겨났다.리액트 같은 라이브러리 혹은 프레임워크를 사용해서 뷰 렌더링을 유저의 브라우저가담당하도록 하고, 어플리케이션을 브라우저에 로드 한 다음에 정말 필요한 데이터만전달받아 보여주는 것이다.※ ‘웹페이지 구동방식에 대한 내용은 하단에 후술하겠다.’하지만 싱글페이지앱이라고 해서 화면의 종류가 하나인 것은 아니다.각 화면 페이지는 저마다 주소가 있고, 주소에 따라 다른 뷰를 보여주는것을바로 라우팅이라고 한다.리액트 자체에는 이 기능이 내장되어있지 않기 때문에 React-router를 사용하는 것이다.b. React-router 사용하기기본적인 라우터를 만들어보자아무런 path가 주어지지 않았을 때 기본적으로 표시되는 라우트이다.src/pages/Home.jsimport React from 'react';const Home = () => { return ( <div> <h2> 홈 </h2> </div> );};export default Home;특정 경로로 들어왔을 때 표시할 페이지도 같은 형식으로 만들어준다. (ex - /about 경로)src/pages/About.jsimport React from 'react';const About = () => { return ( <div> <h2>About</h2> </div> );};export default About;다음 이 컴포넌트를 불러와서 파일로 보낼 수 있도록 인덱스를 만들어준다.src/pages/index.jsexport { default as Home } from './Home';export { default as About } from './About';이제 라우트에 맞춰서 컴포넌트를 구분할 수 있도록 App 컴포넌트를 작성한다.src/App.jsimport React, { Component } from 'react';import { Route } from 'react-router-dom';import { Home, About } from 'pages';class App extends Component { render() { return ( <div> <Route exact path=\"/\" component={Home}/> <Route path=\"/about\" component={About}/> </div> ); }}export default App;라우트를 설정 할 때에는 Route 컴포넌트를 사용하고, 경로는 path 값으로 설정한다.결과 -첫번째 라우트 / 는 Home 컴포넌트를, 두번째 라우트 /about 는 About 컴포넌트가 보여진다.2. 코드 스플리팅(Code Splitting)앞서 언급한 싱글페이지 어플리케이션(SPA)의 단점은 앱을 처음 시작할 때 자바스크립트번들 파일에 어플리케이션에 대한 모든 로직을 불러오는 방식이기 때문에 규모가 커지면용량이 커져서 초기 로딩속도가 지연된다는 것이다.이에 대한 솔루션이 바로 필요에 따라 자바스크립트 번들 파일을 여러개의 파일로 분리시키는코드 스플리팅이다.간단히 말해서 한개의 파일에서 처음부터 모든 것을 불러오는 것이 아니라, 설정해 놓은대로라이브러리나 컴포넌트가 실제로 필요해질 때 불러오게하여 페이지 로딩 속도를 개선하는 것이다.a. 기본적인 코드 스플리팅컴포넌트를 스플리팅 하는 간단한 예제이다.src/SplitMe.jsimport React from 'react';const SplitMe = () => { return <div>필요할때만 불러</div>;};export default SplitMe;src/App.jsimport React, { Component } from 'react';class App extends Component { state = { SplitMe: null }; handleClick = () => { import('./SplitMe').then(({ default: SplitMe }) => { this.setState({ SplitMe }); }); }; render() { const { SplitMe } = this.state; return ( <div> <button onClick={this.handleClick}>Click Me</button> {SplitMe && <SplitMe />} </div> ); }}export default App;결과App 컴포넌트에 버튼 하나를 만들고 위의 컴포넌트를 호출하도록 해보자버튼을 누르면 비동기적으로 SplitMe를 불러와서 state 에 담는다그리고 render 함수에서는 state 안에 있는 SplitMe 가 유효 할 때만 렌더링을 해준다.사실 저 버튼을 클릭하기 전까지 필요가 없는 코드이므로 필요할 때불러와서 사용할 수 있게 되는것이다.※ ‘이렇게 분리된 파일을 청크 파일이라고 부른다.’b. 리액트 라우터와 함께 사용하기'src/pages/About.js'import React from 'react';const About = () => { return <div>About</div>;};export default About;'src/pages/Home.js'import React from 'react';const Home = () => { return <div>Home</div>;};export default Home;index.js 파일을 pages 디렉토리에 만들어준다.이 index.js 파일에서 Abuot을 withSplitting 으로 감싸서 다시 보낸다.pages/index.jsimport withSplitting from '../withSplitting';export const Home = withSplitting(() => import('./Home'));export const About = withSplitting(() => import('./About'));이제 App.js 에서 방금 내보낸 페이지 컴포넌트들을 불러와서 라우터 설정을 한다.src/App.jsimport React, { Component } from 'react';import { Route, Link } from 'react-router-dom';import { About, Home } from './pages';class App extends Component { render() { return ( <div> <ul> <li> <Link to=\"/\">Home</Link> </li> <li> <Link to=\"/about\">About</Link> </li> </ul> <hr /> <Route exact path=\"/\" component={Home} /> <Route path=\"/about\" component={About} /> </div> ); }}export default App;브라우저에서 페이지를 이동해보면 이동할 때 파일을 불러오는것을 볼수 있다.처음부터 파일을 불러오지 않고, 해당 파일이 필요할 때, 비동기적으로 불러와사용하는 것이 바로 ‘코드 스플리팅’이다.3. 서버 사이드 렌더링a. 렌더링이란?어떠한 웹 페이지 접속 시, 그 페이지를 화면에 그려주는 것을 렌더링이라 한다.위에서도 언급한 것처럼 전통적인 방식에서의 웹페이지 구동 방식은요청 시마다 새로고침이 일어나며 서버에 새로운 페이지에 대한 요청을 하는 방식이다.이때, View 가 어떻게 보여질지 또한 서버에서 해석하여 보내는데이러한 방식을 서버 사이드 렌더링이라고 한다.이후 기술의 발전으로 웹에서 제공되는 정보량이 많아지고, 속도 등에서 문제점이 발견되면서서버 사이드 렌더링과 다른 SPA(Single Page Application) 기법이 등장하였다.말 그대로 하나의 페이지만 서버에서 제공하고, View 에 대해서는 클라이언트에서자바스크립트를 통해 렌더링 하는 방식이다이를 클라이언트 사이드 렌더링이라고 한다.간단하게 말해서 보여지는 페이지를 그려주는 주체가 서버인지 혹은 클라이언트 인지에 따라방식이 나뉘는 것이다.b. 서버 사이드 렌더링과 클라이언트 사이드 렌더링 방식의 비교서버 사이드 렌더링의 경우 View를 서버에서 이미 렌더링하여 가져오기 때문에첫 로딩 속도가 매우 빠르다. 또한 서버따로, 클라이언트 따로 작성하던 코드가 하나로 합쳐져일관성 있는 코드를 작성할 수 있다.하지만 View 가 바뀌게 되면 또다시 서버에 요청을 해야 한다는 단점이 있다.클라이언트 사이드 렌더링의 경우 페이지를 읽어들이는 시간, 자바스크립트를 읽어들이는 시간,자바스크립트가 화면을 그리는 시간까지 모두 마쳐야 콘텐츠가 사용자에게 보여진다.여기에 웹 서버에서 데이터라도 가져와야 한다면 그 시간은 더욱 길어진다.즉 초기 구동 속도가 느리다는 단점이 존재한다. 하지만 초기 로딩 후에는 서버에 다시 요청할필요없이 클라이언트 내에서 작업이 이루어지므로 더 빠르다.어떠한 방식이 완벽하다고 할 수 없고 장단점이 존재한다 제공할 서비스가 어느 부분에 초점을 맞춰서진행되느냐에 따라 어떤 방식을 사용해야 할지를 결정해야 할 것이다.최근에는 이 두가지 방법을 적절하게 융합한 방법들도 나오는 것으로 보인다.",
"url": "/router-codesplitting-serverrendering"
}
,
"redux-middleware": {
"title": "Redux-middleware",
"author": "Simplehan",
"category": "",
"content": "1. Redux-middleware 란?리덕스 미들웨어(middleware)는 액션을 디스패치했을 때 리듀서에서 이를 처리하기 전에 사전에 지정된 작업들을 실행한다. 미들웨어는 액션과 리듀서 사이의 중간자라고 볼 수 있다.2. Redux-middleware를 사용하는 이유미들웨어는 객체 대신 함수를 생성하는 액션 생성함수를 작성할 수 있도록 도와주는 것이다. 일반 액셩생성자는 파라미터를 가지고 액션 객체를 생성하는 작업만을 한다. const actionCreator = (parms) => ({action: 'ACTION', parms});만약 특정 액션을 현재 상태에 따라 무시하도록 만드려면 일반 액션생성자로는 할수 없다. 미들웨어는 이를 가능하게 한다. const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; function increment() { return { type: INCREMENT_COUNTER }; } function incrementIfOdd() { return (dispatch, getState) => { const {counter} = getstate(); if(counter % 2 === 0) { return; } dispatch(increment()); }; }결과조건문이 true가 되는 경우에만 디스패치를 실행하게 된다.액션이 리듀서로 이동되는 그 사이를 커스텀 할수 있는 것이다. 리듀서가 액션을 처리하기 전에 미들웨어가 전달받은 액션을 콘솔에 기록하거나, 액션을 취소하거나, 다른 종류의 액션을 추가로 디스패치 할 수 있다.3. 미들웨어 만들기로거 미들웨어 만들기src/lib/loggerMiddleware.js const loggerMiddleware = store => next => action => { /* 미들웨어 내용 */ }여기서 next는 store.dispatch 와 비슷한 역할을 한다. 차이점은 next(action) 을 했을 때 바로 리듀서로 넘기거나, 혹은 미들웨어가 더 있다면 다음 미들웨어 처리가 되도록 진행한다는 것이다. store.dispatch 의 경우에는 처음부터 다시 액션이 디스패치 되는 것이기 때문에 현재 동작한 미들웨어를 다시 한번 처리하게 된다.4. 미들웨어 적용하기미들웨어는 store 를 생성 할 때 설정한다. redux 모듈 안에 들어있는 applyMiddleware 를 사용하여 설정한다.src/store.js import { createStore, applyMiddleware } from 'redux'; import modules from './modules'; import loggerMiddleware from './lib/loggerMiddleware'; // 미들웨어가 여러개인경우에는 파라미터로 여러개를 전달해주면 됩니다. 예: applyMiddleware(a,b,c) // 미들웨어의 순서는 여기서 전달한 파라미터의 순서대로 지정됩니다. const store = createStore(modules, applyMiddleware(loggerMiddleware)) export default store;5. 비동기작업 처리※ Promise 란? Promise는 비동기 처리를 다루기 위해 사용되는 객체이다.비동기적으로 해야 할 작업이 많아져 코드의 구조는 자연스럽게 깊어지다보면 코드를 읽기 힘들어질 것이다. 기존의 자바스크립트 콜백 지옥 문재에서 구제해주는것이 바로 Promise 이다. function printLater(number) { return new Promise( // 새 Promise 를 만들어서 리턴함 (resolve, reject) => { // resolve 와 reject 를 파라미터로 받습니다 setTimeout( // 1초뒤 실행하도록 설정 () => { if(number > 5) { return reject('number is greater than 5'); } // reject 는 에러를 발생시킵니다 resolve(number+1); // 현재 숫자에 1을 더한 값을 반환합니다 console.log(number); }, 1000 ) } ) } printLater(1) .then(num => printLater(num)) .then(num => printLater(num)) .then(num => printLater(num)) .then(num => printLater(num)) .then(num => printLater(num)) .then(num => printLater(num)) .then(num => printLater(num)) .catch(e => console.log(e));몇번을 하던 코드의 깊이는 일정하다. Promise 에서는 값을 리턴하거나, 에러를 발생 시킬 수도 있다.결과 1 2 3 4 5 number is greater than 5Promise를 사용하면 콜백지옥에 빠질일이 없어진다.",
"url": "/redux-middleware"
}
,
"redux": {
"title": "Redux",
"author": "Simplehan",
"category": "",
"content": "1. Redux 란?간단히 얘기해서 redux는 컴포넌트의 개수가 굉장히 많은 프로젝트에서 효율적으로 상태값을 관리할 수 있는 상태관리 라이브러리이다.2. Redux 개념리덕스의 기본개념을 쉽게 설명하면 상태 관리의 로직을 컴포넌트 밖에서 처리한다는 것이다. 리덕스를 사용하면 스토어(store)라는 객체 내부에 상태값을 담게 된다. 이를 사용하면 컴포넌트가 많은 복잡한 형태의 프로젝트에서 상태값에 연관된 모든 컴포넌트들을 업데이트 하고 리렌더링 할 필요 없이 바로 전달할 수 있게 된다는 것이다.a. 스토어: 애플리케이션의 상태 값들을 내장하고 있다. 스토어는 하나만 존재한다.b. 액션: 상태 변화를 일으킬 때 참조하는 객체.c. 디스패치: 액션을 스토어에 전달하는 것을 의미한다.d. 리듀서: 상태를 변화시키는 로직이 있는 함수이다.e. 구독: 스토어 값이 필요한 컴포넌트는 스토어를 구독한다.리덕스의 흐름을 간단히 정리해보면 다음과 같다. 액션을 디스패치에 넣어 동작시키면 스토어안의 리듀서가 스토어의 state 를 변경하고 변경된 state는 이를 구독하고 있는 컴포넌트에 전달한다.이것의 장점은 state의 변화가 예측 가능하다는 점이다.특정 액션이벤트 발생에만 리듀서가 작동하게 했기 때문에 스토어의 state가 변화한 경우 그것이 정확이 어떤 액션 이벤트로부터 변경된 것인지 알수 있다. 또한, 어떤 액션으로부터 변경된 것인지 알기 때문에 해당 state가 변화해온 과정을 마치 뒤로가기 버튼이 있는 것처럼 하나하나 확인해 볼수 있다는 것이다.3. Redux 3가지 기본원칙1. 애플리케이션의 모든 state는 하나의 store 안에 하나의 객체 트리 구조로 저장된다.2. 리덕스의 state 값은 모두 읽기 전용이다3. 모든 state 변화는 순수 함수(리듀서 함수)로 구성해야 한다 또한 순수 함수에서 결과 값을 출력할 때는 파라미터 값에만 의존해야 하며, 같은 파라미터는 언제나 같은 결과를 출력해야 한다4. Redux의 역할리덕스는 효율적으로 상태 관리를 할수 있는 라이브러리이다. 스토어에 상태 정보를 가진 객체를 넣어 두고, 액션이 디스패치되었을 때 리듀서 함수를 이용하여 상태를 변화시키고, 상태가 변화될 때마다 스토어에 구독된 함수를 실행시키는 것이 주요 역할이다.",
"url": "/redux"
}
,
"flutter": {
"title": "Flutter 기본 사용 방법 정리",
"author": "Simplehan",
"category": "",
"content": "1. Flutter 란?Flutter는 모모바일 앱을 개발 할 수 있도록 지원하는 모바일 SDK 입니다.길게 풀어서 쓰면, 웹 개발자이든, native 모바일 개발자 이든관계없이 프레임워크를 사용하지 않고 단순하고 쉽게 만들수 있는 SDK라고 합니다.Google에서 개발하였으며, Dart 언어를 사용하는 특징을 가지고 있습니다.2. Flutter 개발환경 세팅Flutter SDK 다운로드Android Studio 다운로드Visual Studio Code 다운로드먼저 Flutter SDK를 원하는 위치에 압축을 해제합니다. (C:\\Program Files\\와 같이 권한이 필요한 위치에 설치 하지 마세요!)그리고 Flutter 환경 변수를 설정합니다.시스템 변수 : 변수 : 압축을 푼 위치.사용자 변수 : %변수%\\bin cmd에서 flutter doctor 명령 실행이 되는지 확인합니다. 그림과 같이 초록 체크가 되었다면, 정상적으로 설치되었다는 뜻이며 만약 그렇지 않을경우, 어떤게 설치가 안되었는지, 필요한게 무엇인지 알려줍니다.일단, 당연히 안드로이드 스튜디오와 IDE인 VS Code가 미설치되었으므로, 추가로 설치를 진행합니다.다음으로는 안드로이드 스튜디오와 비주얼 스튜디오 코드를 설치할 것인데, 따로 설정해야하는 작업은없으며 정상 설치 후플러그인 환경설정의 마켓플레이스에서 Flutter와 Dart 플러그인을 설치합니다.(VS Code로 사용할 경우에는 추가로 명령 팔레트에서 Flutter와 Dart를 설치합니다.)그 후, 다시 flutter doctor 명령어를 통해 정상 설치 되었는지 확인합니다.3. 기본 프로젝트 생성 및 구조와 UI 작성 방법.저는 VS Code으로 작업하여, VS Code 기준으로 작성하겠습니다.일단 VS Code의 터미널에서 프로젝트를 생성할 경로를 잡아줍니다.그 후 flutter create project_name 명령어를 사용하여, 생성할 수 있습니다.앱의 기본적인 코드는 lib/main.dart에 있으며,기본 구조는 다음과 같습니다. void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), ), body: Center( child: Text('Hello World'), ), ), ); } }앱이 실행 될 때 MyApp 클래스를 실행하여 build를 override하여 출력.MaterialApp의 title은 제목, appBar는 상단 바, body는 내부의 내용을 의미합니다.UI의 작성방법은 내부의 child나 title에 전달되는 Text를 바꾸면 되는데요,예를 들면.. void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), ), body: new Column( children: <Widget>[ new Text(\"1번\"), new Text(\"2번\"), new Text(\"3번\"), ], ), ), ), ); } }이런식으로 작성 가능합니다!혹은, 아예 클래스를 생성하여 넣거나, 색상을 지정하는것도 가능합니다. void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: BodyContainer(), theme: new ThemeData( primarySwatch: Colors.blueGrey ), ); } } class BodyContainer extends StatefulWidget { @override BodyContainerState createState() => BodyContainerState(); } class BodyContainerState extends State<BodyContainer> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('안녕하세요?'), ), body: new Text(\"다른 클래스입니다.\"), ); } }이렇게 작성한 코드를 빌드하려면 flutter run 명령어를 통해 빌드할 수 있으며,기본적인 디버그는 F5키를 눌러 디버그 모드에 진입할 수 있습니다.4. 라이브러리 Import라이브리러 사용법은 pubspec.yaml 파일에서 의존성을 추가하면 됩니다.예를 들면, 폴더 내부에서 dependencies에 english_words: version 을 추가후,Flutter pub get 명령어를 통해 외부 라이브러리를 설치 할 수 있습니다..그 후, import 하는 방법은 main.dart 상단에 import ‘package:english_words/english_words.dart’ 구문을 추가하여 import 할 수 있습니다.5. 조건문, 반복문, 예외처리문 사용 방법. // 1.\tif – else var number = 57; if (number > 100) { print('Large Number'); } else if (number < 100) { print('Small Number'); } else { print('number is 100'); } //또는 삼항 연산자를 사용하여 간단히 구현할수도있습니다! int age = 60; String status = age < 50 ? 'still young' : 'old man'; // 2.\tfor for (int i=0; i<100; i++) { print('number : ' + i); } // 3.\tWhile int i=0; while (i < 10) { print(i); i++; } // 4.\tdo while int i = 0; do { print(i); i++ } while (I < 10); // 5.\tswitch int age = 50; switch (age) { case 10 : print(\"ten\"); break } // 6.\t에외처리 throw new Exception(); //7.\tTry – catch – finally try { divide(10, 0); } catch (e) { print(e); } finally { print('I will always be executed'); }다음에는 간단한 예제를 만들어보고, 안드로이드 포팅 및 IOS 포팅에 대해 알아보려고 합니다.감사합니다.",
"url": "/flutter"
}
,
"react-component-styling": {
"title": "React Component Styling",
"author": "Simplehan",
"category": "",
"content": "1. CSSCSS는 HTML로 표현한 정보를 시각적으로 어떻게 보여지도록 하는지 기술하는 언어이다. 이는 정보와 디자인을 분리하여 관리하기 때문에 매우 유용하다. 그러나 프로젝트의 규모가 커진다면 일반적인 CSS로는 클래스명이 중첩될 가능성이 있다는 단점이 있다.이번 글에서는 CSS의 다른 솔루션을 알아본다.우선 CSS와 관련한 설정정보를 변경해 주어야한다 이는 프로젝트 생성하면 볼수 없도록 내부에 들어있다. 프로젝트를 생성 후 콘솔에서 명령어를 입력하여 밖으로 빼주어야 한다.yarn eject 또는 npm eject적용이 완료되면 Webpack 설정을 변경해야 한다.프로젝트 디렉토리 내에서 아래 파일에 ‘css-loader’를 찾아서 설정을 바꾸어 준다.‘config/webpack.config.dev.js’ { loader: require.resolve('css-lader'), options: { importLoaders: 1, modules: true, localIdentName: '[name]__[local]___[hash:base64:5]' }, },‘config/webpack.config.prod.js’ { loader: require.resolve('css-lader'), options: { importLoaders: 1, modules: true, localIdentName: '[name]__[local]___[hash:base64:5]', minimize: true, sourceMap: true, }, },2. CSS Module설정을 변경 후 yarn start 을 입력하면 일반 CSS가 적용되지 않는다.CSS Module을 사용하면 CSS 파일을 불러온 컴포넌트 내부에서만 작동한다. 적용할 파일에 CSS 파일을 불러올때 래퍼런스 안에 넣어주어 모듈처럼 사용하는 것이다.‘App.css’ .excemple_bluebox { width: 100%; height: 100%; background-color: blue; } import style from './App.css'; class App extends Component { render() { return ( <div className={style.excemple_bluebox}></div> ); } } export default App;※ CSS Module 의 사용법'글로벌 스타일 사용':global .excemple_bluebox { width: 100%; height: 100%; background-color: blue;}'여러 개의 클래스를 적용해야 할 때'<div className={`${style.excemple_bluebox} ${style.excemple_redbox}`}></div>'특정 css를 상속할 때'.excemple_bluebox { composes: redbox; background-color: blue;}'다른 클래스에서 가져와야 할 때'.excemple_bluebox { composes: className from \"./style.css\"; background-color: blue;}3. CSS 전처리기CSS 전처리기는 CSS 문서의 작성에 도움을 주는 도구이다. CSS를 작성할 때 반복적인 작업이 요구되고 클래스의 상속과 같은 사항으로 문서는 양이 많아지고 이로 인해 유지관리에 많은 영향을 준다.CSS 전처리기는 이런 문제점들을 변수, 함수, 상속 등 프로그래밍 개념을 사용해 해결한다.가장 많이 사용되는 전처리기는 SASS, LESS, Stylus 가 있으며, 서로의 특징에 맞게 약간의 구문만 다를 뿐 개념 자체는 동일하다.CSS 전처리기의 장점1. 재사용성 - 공통 요소 또는 반복적인 항목을 변수 또는 함수로 대체2. 유지관리 - 중첩, 상속과 같은 요소로 인해 구조화된 코드로 유지 및 관리가 용이CSS 전처리기의 단점'실무에서의 사용'이다. 실무에서 대부분 CSS 작업은 퍼블리셔(Publisher) 또는 디자이너(Designer)또는 Fropnt-End 개발자가 진행하는 경우도 있는데 문제는 이들이 개발에 대한 역량과 요소를 알아야 한다는 것이다.CSS 전처리기는 프로그래밍 한 요소를 접목했기 때문에 분기문 처리, 변수의 이해, CSS함수의 이해 등개발적인 요소를 알아야 하기 때문이다.CSS 전처리기의 기능을 간단하게 알아보자.ex 1) 우리가 특정 클래스에 color 속성을 주고 값을 지정할 때 hex code 또는 RGB 로 지정한다. 그런데 특정 색상이 반복된다면 우리는 주기적으로 색상 코드를 찾고 반영을 해줘야 한다. 하지만 CSS 전처리기에서는 이런 반복되는 부분을 변수로 처리할 수 있다. 대부분 하나의 파일 안에 color-set 을 모두 정의하여 사용된다. $primary-color: #fff; $secondary-color: #000; .class-A { background-color: $primary-color; } .class-B { background-color: $secondary-color; }ex 2) 각 CSS 전처리기에는 내장함수(Built-in Functions)가 존재한다. 이 내장 함수는 이미 전처리기 내에 포함된 함수로 우리는 필요한 값들만 전달하면 이에 대한 결과를 얻을 수 있다. 예를 들어 darken(color, amount) 라는 내장 함수는 색상과 퍼센티지를 지정해 주면 거기에 알맞는 값을 출력해 준다. $color: #2ecc71; $buttonDark: darken($buttonColor, 10%); .button { background-color: $color; box-shadow: 0px 5px 0px $buttonDark; }ex 3) 유지 관리는 가장 중요하고 눈에 띄는 장점 중 하나이다. 클래스를 정의하다 보면 상속과 공통속성을 지정하기 위해 굉장히 길게 정의해야 할때가 있다. 이런 이유로 CSS 문서가 가독성이 매우 떨어지는데 CSS 전처리기에서는 중첩(Nesting) 과 상속(Extend) 을 통해 구조화 할 수 있다. .class-A { width: 100%; .A-child-1 { color: red; } .A-child-2 { color: blue; } } .class-A { color: red; padding: 10px; } .class-B { @extend: .class-A; border: 1px solid red; }위 내용을 컴파일하면 아래와 같다. .class-A { width:100%; } .class-A .A-child-1 { color: red; } .class-A .A-child-2 { color: blue; } .class-A, .class-B { color: red; padding: 10px; } .class-B { border: 1px solid red; }이처럼 CSS 전처리기를 사용하면 많은 도움이 된다. 하지만 그 전처리기를 사용하는 실무자가 개발자가 아닐수 있다.이상 CSS에 대해 알아보았다.",
"url": "/react-component-styling"
}
,
"add-summernote": {
"title": "Spring 프로젝트에 Summernote 적용하기",
"author": "Simplehan",
"category": "",
"content": "이전에 작성한 Naver SmartEditor 포스트에 이어서 이번엔 다른 위지윅 에디터를 적용해보기로 했다. 이번에 적용해 볼 에디터는 Summernote라는 에디터이다. bootstrap, jQuery 기반의 에디터여서 필자 기준으로는 SmartEditor보다 더 쉽게 사용을 할 수 있었다. SmartEditor에선 몇번 헤딩을 한 이미지 업로드도 Summernote에선 너무나도 쉽게 구현을 하였다.이전에 진행한 Bootstrap을 적용한 Spring 프로젝트를 이용해서 진행한다.Spring 프로젝트에 Bootstrap 적용하기Summernote 리소스 받기일단 처음으로 Summernote 다운로드 페이지에서 리소스를 다운받자. 필자는 zip 파일을 다운받았다.다운받은 zip 파일의 압축을 푼 뒤, dist폴더 내부의 파일들을 src/resources/libs/summernote에 넣어주자.코드 생성다음으로 summernote용 controller와 view를 작성해보자. 먼저 com.example.demo.summernote.controller 패키지를 만들고 SummernoteController.java 파일을 만들자.package com.example.demo.summernote.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class SummernoteController {\t@RequestMapping(\"/summernote\")\tpublic String summernote() {\t\treturn \"summernote\";\t}}그 다음 JSP 파일을 만들자. 이름은 controller에서 지정해준 것 처럼 summernote.jsp로 만들었다.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link id=\"contextPath\" data-path=\"<c:url value='/'/>\" />\t\t<link href=\"https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\" rel=\"stylesheet\">\t\t<link rel=\"stylesheet\" href=\"<c:url value='/libs/summernote/summernote.min.css'/>\"/>\t\t\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t\t<script src=\"https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js\"></script>\t\t<script src=\"<c:url value='/libs/summernote/summernote.min.js'/>\"></script>\t\t<script src=\"<c:url value='/js/summernote/summernote.js'/>\"></script>\t\t\t\t<title>Insert title here</title>\t</head>\t<body>\t\t<div id=\"summernote\">Hello Summernote</div>\t</body></html>유심히 봐야할 부분이 있다. 그간 포스트 작성하며 사용해 온 bootstrap은 4버전인데, summernote는 bootstrap 3버전에서 운용해줘야 깔끔하게 돌아간다. 이에 bootstrap css와 js 부분을 CDN을 통해서 3.3.7 버전을 내려받아서 사용하였다. 만약 3.3.7을 따로 가지고 있는 사람들은 해당 리소스를 사용하면 될 것이다.그리고 추가로 src/resources/static/js/summernote/summernote.js 파일을 추가한 뒤 아래 코드를 작성하자.$(document).ready(function() {\t$('#summernote').summernote();});이제 브라우저에서 http://localhost:8080/summernote로 접속하여 summernote 에디터가 잘 뜨는지 확인해보자.summernote는 이미지 업로드 시 해당 이미지를 base64로 변환한 뒤 바로 코드로 적용하도록 동작한다. 완전히 클라이언트 단에서 모든 작업이 이루어지게 되어 있는데, 이를 이대로 사용할 수도 있지만 서버에 작성한 HTML 코드를 전송했을 때 base64의 어마어마한 텍스트가 같이 전달된다. 이미지를 10개, 20개 추가한 뒤 서버로 보내게 되면 텍스트의 양은 어마어마해져서 이를 조금 수정하려고 한다.작동 방식을 다음과 같이 바꿔봤다. 에디터에서 이미지 선택 서버에 이미지를 전송하여 서버에 이미지 저장 에디터 코드에서 img 태그를 사용하여 업로드 한 서버 이미지 호출문제는 이렇게 코드를 적용시키면 추후에 이미지를 추가했다가 지워버리면 해당 이미지가 temp로 남아버리기 때문에 temp 파일 관리가 추가로 들어가줘야 한다.일단은 위 로직부터 구현을 해보자.파일 업로드 & 조회 로직 구현위 로직을 구현하려면 파일 업로드와 파일 조회 로직을 구현해야 한다. 이건 추후 포스팅으로?",
"url": "/add-summernote"
}
,
"add-smarteditor": {
"title": "Spring 프로젝트에 Naver SmartEditor 적용하기",
"author": "Simplehan",
"category": "",
"content": "네이버 블로그, 카페 등을 이용할 때, 웹상에서 글쓰기를 진행하다보면 폰트설정, 링크넣기, 이미지넣기 등등 부가적인 기능이 다양하게 들어간 에디터를 번번히 사용하게 된다. 이러한 에디터들을 위지윅 에디터(WYSIWYG Editor)라고 하며 Naver에선 Smart Editor 2 버전을 무료로 공개중이다. 이번 포스트에서는 해당 Smart Editor를 적용해볼까 한다.이전에 진행한 Bootstrap을 적용한 Spring 프로젝트를 이용해서 진행한다.Spring 프로젝트에 Bootstrap 적용하기SmartEditor 리소스 받기SmartEditor github 다운로드 페이지에서 배포판을 다운받을 수 있다. 처음엔 zip 파일을 다운받아서 진행했는데 뭔가 파일이 빠져있는 것 같았다. 안에 skin.html에서 경로에 없는 파일을 참조하고 있길래 zip 파일 리소스는 다 지우고 tgz 파일을 다시 받아서 진행했다.tgz 파일의 압축을 풀어보면 package 폴더가 나오고 안에 들어가면 뭔가 파일이 몇개 있는데, 다 무시하고 dist 폴더 안의 내용만 있으면 된다. (smarteditor2-2.10.0/package/dist)해당 폴더 안의 파일들을 전부 복사해서 src/resources/static/libs/smarteditor 폴더에 넣어주자.코드 생성다음으로 smarteditor 용 controller와 view를 작성해보자. 먼저 com.example.demo.smarteditor.controller 패키지를 만들고 SmartEditorController.java 파일을 만들자.package com.example.demo.smarteditor.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class SmartEditorController {\t@RequestMapping(\"/smarteditor\")\tpublic String smartEditor() {\t\treturn \"smartEditor\";\t}}그 다음 JSP 파일을 만들자. 이름은 controller에서 지정해준 것 처럼 smartEditor.jsp로 만들었다. js 코드는 간단하기 때문에 jsp 내부 하단에 바로 작성하였다.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link id=\"contextPath\" data-path=\"<c:url value='/'/>\" />\t\t<link rel=\"stylesheet\" href=\"<c:url value='/libs/bootstrap-4.4.1-dist/css/bootstrap.min.css'/>\">\t\t\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t\t<script src=\"<c:url value='/libs/bootstrap-4.4.1-dist/js/bootstrap.min.js'/>\"></script>\t\t<script type=\"text/javascript\" src=\"<c:url value='/libs/smarteditor/js/service/HuskyEZCreator.js'/>\" charset=\"utf-8\"></script>\t\t\t\t<title>Insert title here</title>\t</head>\t<body>\t\t<textarea name=\"ir1\" id=\"ir1\" rows=\"10\" cols=\"100\">에디터에 기본으로 삽입할 글(수정 모드)이 없다면 이 value 값을 지정하지 않으시면 됩니다.</textarea>\t</body>\t\t<script>\t\tvar oEditors = [];\t\tnhn.husky.EZCreator.createInIFrame({\t\t\toAppRef: oEditors,\t\t\telPlaceHolder: \"ir1\",\t\t\tsSkinURI: $('#contextPath').data('path') + \"libs/smarteditor/SmartEditor2Skin.html\",\t\t\tfCreator: \"createSEditor2\"\t\t});\t</script></html>이제 브라우저에서 http://localhost:8080/smarteditor 로 접속해보자. 아래와 같이 나오면 성공이다.에디터에 작성한 내용 JS에서 불러오기에디터를 열심히 만들어놓고 정작 내부의 내용을 가져오지 못하면 아무 소용이 없다. 내부 내용을 가져오는 코드를 작성해보자. 일단 JSP 파일 먼저 아래처럼 수정한다.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link id=\"contextPath\" data-path=\"<c:url value='/'/>\" />\t\t<link rel=\"stylesheet\" href=\"<c:url value='/libs/bootstrap-4.4.1-dist/css/bootstrap.min.css'/>\">\t\t\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t\t<script src=\"<c:url value='/libs/bootstrap-4.4.1-dist/js/bootstrap.min.js'/>\"></script>\t\t<script type=\"text/javascript\" src=\"<c:url value='/libs/smarteditor/js/service/HuskyEZCreator.js'/>\" charset=\"utf-8\"></script>\t\t\t\t<title>Insert title here</title>\t</head>\t<body>\t\t<textarea name=\"ir1\" id=\"ir1\" rows=\"10\" cols=\"100\">에디터에 기본으로 삽입할 글(수정 모드)이 없다면 이 value 값을 지정하지 않으시면 됩니다.</textarea>\t\t<button type=\"button\" class=\"btn\">작성내용 콘솔에 찍기!!</button>\t</body>\t\t<script>\t\tvar oEditors = [];\t\tnhn.husky.EZCreator.createInIFrame({\t\t\toAppRef: oEditors,\t\t\telPlaceHolder: \"ir1\",\t\t\tsSkinURI: $('#contextPath').data('path') + \"libs/smarteditor/SmartEditor2Skin.html\",\t\t\tfCreator: \"createSEditor2\"\t\t});\t\t$(\".btn\").on(\"click\", function() {\t\t\toEditors.getById[\"ir1\"].exec(\"UPDATE_CONTENTS_FIELD\", []);\t\t\tvar value = document.getElementById(\"ir1\").value;\t\t\tconsole.log(value);\t\t});\t</script></html>버튼을 하나 추가했고, 해당 버튼에 이벤트를 하나 추가했다. 이제 에디터에 아무 내용을 작성한 뒤 개발자 도구의 콘솔창을 한번 봐보자.콘솔에 HTML 구조로 찍힌다면 성공했다.추가로 이미지를 추가하는 내용을 작성하려고 했는데, 네이버측에서 이미지를 추가하는 예제가 취약점이 발생되어서 최신 버전에는 해당 예시를 없앤 뒤 배포중이다. 이전 버전을 다운받으면 해당 예시를 볼 수 있는데 PHP, Jindo 기반으로 되어 있어서 이를 JSP, JAVA, jQuery 기반으로 적용해보다가 아직 적용을 하지 못해서 작성하지 못했다..추후 커스터마이징을 완료하고 이미지 추가가 정상작동된다면 포스트를 추가할 예정이다.",
"url": "/add-smarteditor"
}
,
"add-websocket": {
"title": "Spring 프로젝트에 WebSocket 적용하기",
"author": "Simplehan",
"category": "",
"content": "웹 프로젝트를 진행하다보면 단순히 Restful API로 처리할 수 없는 요건이 생길 수 있다. 보통 HTTP 통신은 클라이언트에서 Request를 날린 후 서버의 Response를 받는 형식인데, Server에서부터 통신이 시작되야되는 요건이 있을 수 있다. 혹은 지속적으로 connection을 맺고 있어야 한다거나…(실시간 채팅 혹은 알림 서비스가 그런 요건에 속한다.)이를 구현할 때는 HTTP 프로토콜을 이용하여 Polling 방식을 통해 구현할 수도 있지만, 이런 요구조건을 맞추기 위해 WebSocket API가 등장했다. 이름 그대로 Web 상에서 Socket 방식으로 통신을 구현한다.WebSocket은 구버전 브라우저에선 지원하지 않는 다는 단점이 있는데, 이를 보완하기 위해 SockJS라는 모듈이 있다. SockJS는 구버전 브라우저에서도 WebSocket 코드가 그대로 동작하도록 도와준다. (polyfill로 볼 수 있을 듯 하다.)본 포스트는 Spring에서 제공하는 가이드를 보고 만들었기 때문에 자세한 사항은 아래 링크 가이드에서 볼 수 있다.Spring WebSocket 적용 가이드또한 이전에 진행한 Bootstrap을 적용한 Spring 프로젝트를 이용해서 진행한다.Spring 프로젝트에 Bootstrap 적용하기WebSocket 모듈 적용프로젝트에 WebSocket 모듈을 적용해보자. pom.xml에 아래 코드를 통해 모듈을 추가하자.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- websocket --><dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId></dependency><dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version></dependency><dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version></dependency>이번 포스트에서는 가이드대로 webjars 기술을 이용해보았다. webjars는 간단히 말하면 웹 라이브러리 의존성 관리 도구이다. 웹 라이브러리를 JAR 파일 안에 패키징해준다. 그렇기 때문에 따로 라이브러리를 다운로드 받을 필요 없이 /webjars path를 통해서 라이브러리를 사용할 수 있게 해준다.JAVA 코드 생성그 다음 두가지 객체를 만들어 준다. 하나는 HelloMessage.java 파일로 클라이언트에서 입력한 이름을 받는 객체이다. 나머지 하나는 Greeting.java 파일로 받은 이름과 같이 클라이언트쪽으로 응답할 때 사용하는 객체이다. 일단 com.example.demo.websocket.vo 패키지를 만든 뒤 아래 코드와 같이 객체를 추가해주자.package com.example.demo.websocket.vo;public class HelloMessage {\tprivate String name;\tpublic HelloMessage() {}\tpublic HelloMessage(String name) {\t\tthis.name = name;\t}\tpublic String getName() {\t\treturn name;\t}\tpublic void setName(String name) {\t\tthis.name = name;\t}}package com.example.demo.websocket.vo;public class Greeting {\tprivate String content;\tpublic Greeting() {}\tpublic Greeting(String content) {\t\tthis.content = content;\t}\tpublic String getContent() {\t\treturn content;\t}}그 다음 com.example.demo.websocket.web 패키지를 만든 뒤 GreetingController.java 파일을 추가하자. 코드는 아래와 같다.package com.example.demo.websocket.web;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.stereotype.Controller;import org.springframework.web.util.HtmlUtils;import com.example.demo.websocket.vo.Greeting;import com.example.demo.websocket.vo.HelloMessage;@Controllerpublic class GreetingController {\t@MessageMapping(\"/hello\")\t// 요청을 받는 곳\t@SendTo(\"/topic/greetings\")\t// 괄호 안의 메시지 브로커에 return 값을 보낸다.\tpublic Greeting greeting(HelloMessage message) throws Exception {\t\tThread.sleep(1000);\t\t// 수동 딜레이\t\treturn new Greeting(\"Hello, \" + HtmlUtils.htmlEscape(message.getName()) + \"!\");\t} @RequestMapping(\"/websocket\")\tpublic String websocket() {\t\treturn \"websocket\";\t}}이제 JSP에서 테스트 코드를 작성하기 전에, WebSocket 설정 파일을 작성하자. com.example.demo.config 패키지를 만들고 WebSocketConfig.java 파일을 작성하자. 코드는 아래와 같다.package com.example.demo.config;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\t@Override\tpublic void configureMessageBroker(MessageBrokerRegistry config) {\t\tconfig.enableSimpleBroker(\"/topic\");\t// /topic path에 메시지 브로커 생성\t\tconfig.setApplicationDestinationPrefixes(\"/app\");\t// 요청받는 path의 앞단\t}\t\t@Override\tpublic void registerStompEndpoints(StompEndpointRegistry registry) {\t\tregistry.addEndpoint(\"/gs-guide-websocket\").withSockJS();\t// WebSocket 기본 path\t}}JSP, JS 코드 생성이제 JSP 파일을 추가해보자. JSP 파일은 src/main/webapp/WEB-INF/jsp 폴더 안에 websocket.jsp로 만들고 코드는 아래와 같이 작성했다.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link rel=\"stylesheet\" href=\"<c:url value='/libs/bootstrap-4.4.1-dist/css/bootstrap.min.css'/>\">\t\t<script src=\"/webjars/sockjs-client/sockjs.min.js\"></script>\t\t<script src=\"/webjars/stomp-websocket/stomp.min.js\"></script>\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t\t<script src=\"<c:url value='/libs/bootstrap-4.4.1-dist/js/bootstrap.min.js'/>\"></script>\t\t<title>Insert title here</title>\t</head>\t<body>\t\t<section id=\"main-content\">\t\t\t<section class=\"wrapper\">\t\t\t\t<h3><i class=\"fa fa-angle-right\"></i> WebSocket</h3>\t\t\t\t<div class=\"row\">\t\t\t\t <div class=\"col-md-6\">\t\t\t\t <form class=\"form-inline\">\t\t\t\t <div class=\"form-group\">\t\t\t\t\t\t\t\t<label for=\"connect\">WebSocket connection:</label>\t\t\t\t <button id=\"connect\" class=\"btn btn-default\" type=\"submit\">Connect</button>\t\t\t\t <button id=\"disconnect\" class=\"btn btn-default\" type=\"submit\" disabled=\"disabled\">Disconnect\t\t\t\t </button>\t\t\t\t </div>\t\t\t\t </form>\t\t\t\t </div>\t\t\t\t <div class=\"col-md-6\">\t\t\t\t <form class=\"form-inline\">\t\t\t\t <div class=\"form-group\">\t\t\t\t <label for=\"name\">What is your name?</label>\t\t\t\t <input type=\"text\" id=\"name\" class=\"form-control\" placeholder=\"Your name here...\">\t\t\t\t </div>\t\t\t\t <button id=\"send\" class=\"btn btn-default\" type=\"submit\">Send</button>\t\t\t\t </form>\t\t\t\t </div>\t\t\t\t</div>\t\t\t\t<div class=\"row\">\t\t\t\t <div class=\"col-md-12\">\t\t\t\t <table id=\"conversation\" class=\"table table-striped\">\t\t\t\t <thead>\t\t\t\t <tr>\t\t\t\t <th>Greetings</th>\t\t\t\t </tr>\t\t\t\t </thead>\t\t\t\t <tbody id=\"greetings\">\t\t\t\t </tbody>\t\t\t\t </table>\t\t\t\t </div>\t\t\t\t</div>\t\t\t</section>\t\t</section>\t</body></html>브라우저에서 키보드의 F12키를 눌러서 개발자 도구를 연 뒤 Network탭을 클릭하자. 그 후 브라우저를 새로고침한 뒤 sockjs 모듈과 stomp 모듈이 제대로 호출되는지 확인해보자. webjars를 이용하여 라이브러리를 직접 추가해주지 않아도 호출되는 모습을 확인할 수 있다.마지막으로 websocket.js 파일을 추가해주려 한다. src/resources/static/js/websocket 폴더 내에 websocket.js 파일을 추가해주자.그 다음 아래 코드를 입력하자.var stompClient = null;// connected 상태 변경function setConnected(connected) { $(\"#connect\").prop(\"disabled\", connected); $(\"#disconnect\").prop(\"disabled\", !connected); if (connected) { $(\"#conversation\").show(); } else { $(\"#conversation\").hide(); } $(\"#greetings\").html(\"\");}// WebSocket 접속function connect() { var socket = new SockJS('/gs-guide-websocket');\t// WebSocketConfig.java에서 설정한 endpoint stompClient = Stomp.over(socket);\t// socket을 이용하여 stompClient 생성 // socket 접속 stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); // '/topic/greetings' 메시지 브로커 구독 // 해당 토픽에 메시지가 게시되면 이 클라이언트가 메시지를 읽어서 클라이언트의 콜백함수를 통해 메시지 전달 stompClient.subscribe('/topic/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).content); }); });}// WebSocket 접속 끊기function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log(\"Disconnected\");}// message 보내기function sendName() { stompClient.send(\"/app/hello\", {}, JSON.stringify({'name': $(\"#name\").val()}));}// 서버로부터 받은 메시지 출력function showGreeting(message) { $(\"#greetings\").append(\"<tr><td>\" + message + \"</td></tr>\");}$(function () { $(\"form\").on('submit', function (e) { e.preventDefault(); }); $( \"#connect\" ).click(function() { connect(); }); $( \"#disconnect\" ).click(function() { disconnect(); }); $( \"#send\" ).click(function() { sendName(); });});websocket.jsp 파일에 아래 코드를 추가해준다....<script src=\"<c:url value='/js/websocket/websocket.js'/>\"></script>...테스트해보기이제 브라우저를 새로고침하여 개발자도구 콘솔에서 에러가 없는지 확인 후, Connect 버튼을 눌러보자. Connect 버튼을 누른 뒤에 개발자 도구의 콘솔에 아래 이미지와 같이 나타나면 성공이다.이제 Connected 상태에서 오른쪽 What is your name?이라 써있는 간단한 폼에 글자를 입력한 뒤 Send버튼을 클릭해보자. 정상작동됬다면 아래 이미지처럼 뜰 것이다.마지막으로 Disconnect 버튼을 클릭하여 연결을 끊어보자. 이 역시 정상작동하면 아래 이미지처럼 나올 것이다.JAVA에서 어노테이션 외에 코드상으로 WebSocket 사용하기위에서 WebSocket의 동작을 확인해봤다. 하지만 클라이언트에서 요청을 하면 해당 요청을 매핑하여 받은 뒤 브로커로 보내는 방식이 얼핏보면 Restful API와 별다를 바가 없어 보인다. 물론 내부적으로는 socket을 통해서 메시지 브로커가 토픽상의 메시지를 주고 받는 것이지만..이런 식의 구현이 아닌, JAVA에서 무언가 일을 처리한 뒤 메시지 브로커로 보내는 방법은 없을까 싶어서 찾아보니 코드상으로도 메시지 브로커에 메시지를 보내는 방법이 있었다. 단순히 웹 상에서 요청을 받았을 때 메시지 브로커로 데이터를 보내는 것이 아니라 서버가 구동되는 순간이라던지, 스케쥴러가 돌아간 순간이라던지 순수 JAVA 코드가 실행된 후에 메시지 브로커로 데이터를 보낼 수 있는 방법이다.하지만 본 포스트에 사용된 프로젝트로는 마땅한 예제를 만들기엔 조금 힘들어서 예제 코드로만 설명을 하려 한다.일단 com.example.demo.websocket.service 패키지를 만든 뒤 WebSocketService.java 파일을 만들자.코드는 아래처럼 추가해주자.package com.example.demo.websocket.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.messaging.simp.SimpMessagingTemplate;import org.springframework.stereotype.Service;import com.example.demo.websocket.vo.Greeting;@Servicepublic class WebSocketService {\t\t@Autowired\tprivate SimpMessagingTemplate template;\t\tpublic void myTestService() {\t\t// '/topic/greetings' 토픽에 메시지 게시\t\ttemplate.convertAndSend(\"/topic/greetings\", new Greeting(\"Hello\"));\t}}이제 WebSocketService의 myTestService 메소드를 호출하면 /topic/greetings토픽에 두번째 파라미터를 데이터로 게시하게 된다. 이는 필요한 부분에 커스터마이징하여 사용하면 될 것으로 보인다.",
"url": "/add-websocket"
}
,
"add-bootstrap": {
"title": "Spring 프로젝트에 Bootstrap 적용하기",
"author": "Simplehan",
"category": "",
"content": "프론트엔드를 개발하다보면 UI 개발도 같이 진행이 되는데, 이를 개발자가 직접 CSS를 작성하면서 개발하게 되면 개발 기간이 많이 늘어날 수 밖에 없다. 이럴 때 미리 개발되어 있는 UI 프레임워크 등을 가져다 쓰면 편리하지 않을까?다행히 프론트엔드 프레임워크 중에 Bootstrap이라는 프레임워크가 있는데 이 프레임워크를 이용해서 UI 단 개발의 공수를 많이 줄일 수 있다. 이제부터 이 Bootstrap을 저번에 만든 Spring 기본 프로젝트에 적용하여 사용해보자.저번에 만든 Spring 기본 프로젝트 관련 포스트는 아래 링크에서 확인할 수 있다.Spring 프레임워크를 이용한 프로젝트 생성 (with STS, JSP)Bootstrap 다운받기 및 적용Bootstrap을 적용하는 방법엔 크게 3가지가 있다. 리소스를 직접 다운받아서 내 프로젝트에 적용하는 방법과 CDN 서비스를 이용하여 네트워크 상에서 바로 받아서 사용하는 방법, 마지막으로 패키지 매니저(npm, yarn 등등)를 이용하여 받는 방법이다. CDN을 사용하게 되면 다운로드 받고 내 프로젝트에 적용하는 번거로운 작업 없이 바로 Bootstrap을 사용할 수 있지만, 해당 CDN 서버가 다운되거나 뭔가 문제가 생기면 내 프로젝트에도 영향이 간다는 단점이 있다. 안정성을 높이려면 리소스를 직접 다운로드 받아서 사용하는게 좋겠지만 이 방법은 버전 관리가 귀찮다는 단점이 있다. 패키지 매니저를 이용한 방법은 버전 관리도 직접 다운로드보다 수월하고, 리소스 자체도 내려받기 때문에 패키지 매니저를 통해 다운로드 받는 순간만 패키지 매니저 서버가 살아있으면 리소스는 내 프로젝트 내부에서 사용할 수 있다. 하지만 이용하려는 프로젝트 환경 상 패키지 매니저를 지원하지 않는다면 해당 환경을 잡는데 좀 힘들 수 있다.본 포스트에서는 두가지 방법을 소개 해보려 하는데, 직접 다운로드 받는 방법과 CDN 서비스를 이용하는 방법이다. CDN을 이용한 적용은 JSP 파일 내부의 css, js의 링크 값만 바꿔주면 되기 때문에 중간에 같이 소개하려 한다.일단 Bootstrap 다운로드 사이트에 접속하여 Bootstrap 리소스를 다운받자.다운로드 받은 리소스를 내 프로젝트의 src/main/resources/static/libs 폴더 안에 넣어주자.추가로 bootstrap 사용에 jQuery가 필요하므로 jQuery 다운로드 사이트에서 jQuery도 다운받아주자. (jQuery 역시 CDN, 패키지 매니저를 통해 적용할 수 있다.)필요한 모듈을 다 받았다면 JSP 파일에 아래 코드를 추가하여 모듈을 적용해주자.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html> <head> <meta charset=\"EUC-KR\"> <link rel=\"stylesheet\" href=\"<c:url value='/libs/bootstrap-4.4.1-dist/css/bootstrap.min.css'/>\"> <title>Insert title here</title>\t</head>\t<body>\t\tHello world!<br/>\t\tMy name is ${person.name}<br/>\t\tI'm ${person.age} years old<br/>\t</body>\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t<script src=\"<c:url value='/libs/bootstrap-4.4.1-dist/js/bootstrap.min.js'/>\"></script></html>CDN 서비스를 이용하려면 bootstrap, jQuery를 다운로드 받을 필요 없이 아래와 같이 코드를 적용하면 된다.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css\" integrity=\"sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh\" crossorigin=\"anonymous\">\t\t<title>Insert title here</title>\t</head>\t<body>\t\tHello world!<br/>\t\tMy name is ${person.name}<br/>\t\tI'm ${person.age} years old<br/>\t</body>\t\t<script src=\"https://code.jquery.com/jquery-3.4.1.slim.min.js\" integrity=\"sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n\" crossorigin=\"anonymous\"></script> <script src=\"https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js\" integrity=\"sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo\" crossorigin=\"anonymous\"></script> <script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js\" integrity=\"sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6\" crossorigin=\"anonymous\"></script></html>코드 적용 후 브라우저에서 키보드의F12키를 눌러서 개발자 도구를 켠 뒤에 Network탭을 클릭한 뒤 브라우저를 새로고침 해보자. 그 후 아래처럼 리소스를 에러없이 불러오면 성공이다.그 다음 JSP 파일을 아래 코드와 같이 수정한 뒤 브라우저를 새로고침하여 bootstrap이 잘 적용되었는지 확인해보자.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><%@ taglib prefix=\"c\" uri=\"http://java.sun.com/jsp/jstl/core\"%><!DOCTYPE html><html>\t<head>\t\t<meta charset=\"EUC-KR\">\t\t<link rel=\"stylesheet\" href=\"<c:url value='/libs/bootstrap-4.4.1-dist/css/bootstrap.min.css'/>\">\t\t<title>Insert title here</title>\t</head>\t<body>\t\t<!-- Navigation -->\t\t<nav class=\"navbar navbar-expand-lg navbar-dark bg-dark static-top\">\t\t <div class=\"container\">\t\t <a class=\"navbar-brand\" href=\"#\">\t\t <img src=\"http://placehold.it/150x50?text=Logo\" alt=\"\">\t\t </a>\t\t <button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarResponsive\" aria-controls=\"navbarResponsive\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\t\t <span class=\"navbar-toggler-icon\"></span>\t\t </button>\t\t <div class=\"collapse navbar-collapse\" id=\"navbarResponsive\">\t\t <ul class=\"navbar-nav ml-auto\">\t\t <li class=\"nav-item active\">\t\t <a class=\"nav-link\" href=\"#\">Home\t\t <span class=\"sr-only\">(current)</span>\t\t </a>\t\t </li>\t\t <li class=\"nav-item\">\t\t <a class=\"nav-link\" href=\"#\">About</a>\t\t </li>\t\t <li class=\"nav-item\">\t\t <a class=\"nav-link\" href=\"#\">Services</a>\t\t </li>\t\t <li class=\"nav-item\">\t\t <a class=\"nav-link\" href=\"#\">Contact</a>\t\t </li>\t\t </ul>\t\t </div>\t\t </div>\t\t</nav>\t\t\t\t<!-- Page Content -->\t\t<div class=\"container\">\t\t <h1 class=\"mt-4\">Logo Nav by Start Bootstrap</h1>\t\t <p>The logo in the navbar is now a default Bootstrap feature in Bootstrap 4! Make sure to set the width and height of the logo within the HTML or with CSS. For best results, use an SVG image as your logo.</p>\t\t</div>\t\t<!-- /.container -->\t</body>\t\t<script src=\"<c:url value='/libs/jquery-3.4.1.min.js'/>\"></script>\t<script src=\"<c:url value='/libs/bootstrap-4.4.1-dist/js/bootstrap.min.js'/>\"></script></html>",
"url": "/add-bootstrap"
}
,
"start-spring": {
"title": "Spring 프레임워크를 이용한 프로젝트 생성 (with STS, JSP)",
"author": "Simplehan",
"category": "",
"content": "STS (Spring Tool Suite) 받기Spring 프레임워크를 이용하여 프로젝트를 만들기위해 IDE를 정해야하는데, 여러가지 IDE가 있겠지만 Spring 프로젝트 제작시에는 STS를 이용하는게 좋은 방법이라 생각된다. 일단 이클립스 기반이기 때문에 이클립스로 JAVA 프로젝트를 개발하던 개발자들도 익숙하게 사용이 가능하고, 무료로 이용할 수 있다. Spring 팀에서 제작했기 때문에 Spring 프레임워크와 호환도 좋다. 이름부터가 Spring Tool Suite 이니..그 외에 Spring 팀에서 제공하는 IDE가 더 있는데 Visual Studio Code 기반인 IDE와 Theia 기반인 IDE가 있다. 하지만 본 포스트에서는 STS만 다룬다.STS를 받기 위해선 STS 사이트에 접속해야한다. 아래 주소를 통해 접속할 수 있다.Spring ToolsSTS 사이트에 접속하면 아래 사진과 같이 IDE 다운로드 링크가 있으니 필요한 걸 받으면 된다. 본 포스트는 Windows 버전으로 진행한다. 버전은 4.5.1.RELEASE 버전으로 진행한다.Windows 버전을 다운받으면 jar 파일을 내려받게 된다. 이 jar파일의 압축을 풀면 contents 폴더가 나오게 되는데 이 안에 STS가 있다.Spring 프로젝트 생성다운받은 STS를 실행하면 아래와 같이 뜬다.좌측 Package Explorer 창에서 Create new Spring Starter Project를 클릭하면 Spring 프로젝트의 생성을 도와주는 프롬프트가 나오게 된다.https://start.spring.io 에서 프로젝트 생성에 필요한 정보를 받아올 수 있다. Spring 프로젝트는 Web상에서 생성할 수도 있는데 해당 링크에 들어가서 필요한 정보를 입력, 체크한 뒤 Generate를 하면 프로젝트를 내려받을 수가 있다. 궁금하면 링크에 접속해보자.필요한 정보를 입력한 뒤 Next를 클릭하자.Next를 입력하면 Spring에서 제공하는 서비스 모듈을 선택하여 프로젝트 생성 시 같이 포함하여 생성할 수 있다. 여기서 필요한 모듈을 선택하면 되는데, 본 포스트에서는 아래와 같이 Web의 Spring Web 항목만 체크하여 만들었다.필요한 모듈을 모두 체크하였으면 Finish버튼을 클릭하여 프로젝트를 생성한다.프로젝트가 생성되면 구조는 위의 이미지와 같다.my-project├── src| ├── main| | ├── java # JAVA 파일| | └── resources # 정적 파일 (css, js, image, properties 등등)| └── test| └── java # 테스트 용 JAVA 파일 (JUnit 등)├── JRE System Library # JRE├── Maven Dependencies # Maven 의존 모듈├── target # Build 결과물 보관 장소├── HELP.md # Spring 프로젝트 도움말 및 가이드├── mvnw # Maven Wrapper 스크립트├── mvnw.cmd # Maven Wrapper 스크립트의 배치 버전└── pom.xml # Maven 빌드 옵션 설정 파일(위의 구조는 폴더 구조가 아닌 Package Explorer에서 표출해주는 구조이다.)웹페이지 생성 및 접속해보기예전에 이클립스, 전자정부 프레임워크, 스프링 프레임워크를 이용하여 개발할때 서버를 띄우기 위해서 톰캣을 같이 설치하여 톰캣으로 서버를 띄워줘야 했으나, 요즘 스프링 프레임워크는 스프링부트의 내장톰캣을 이용하여 서버를 띄울 수 있기 때문에 별도로 톰캣을 설치할 필요가 없다.(운영환경이라면 톰캣의 커스터마이징이 필요할 수 있기 때문에 톰캣 설치를 추천한다.)다만 스프링부트는 기본적으로 JSP를 지원하지 않기 때문에 JSP를 사용하기 위해선 별도로 메이븐을 통해 모듈을 받아야 한다. 당장에 필요한 모듈은 JSP를 구동시켜주는 내장톰캣 모듈과 JSTL을 지원해주는 JSTL 모듈이기 때문에 해당 모듈들을 pom.xml에 지정해주자.<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId></dependency><dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId></dependency>모듈 추가가 완료됬다면 다음으로 java 파일을 추가해보자. MVC 패턴을 적용하여 페이지를 만들 예정이다. 그렇기 때문에 일단 Controller를 만들어준다. 기본 생성된 프로젝트는 아래 이미지와 같이 스프링부트 기본 java 파일만 있다.여기에 패키지를 추가하고 Controller를 추가해보자. 패키지는 com.example.demo.main으로 만들고, 그 아래에 com.example.demo.main.controller 패키지를 하나 더 추가한 뒤 해당 패키지에 controller를 만든다. 아래 이미지를 보자. (controller 패키지는 web 패키지로 사용하는 경우도 있다. // com.example.demo.main.web)MainController.java는 아직 아무 내용도 없다. 이제 이 java 파일이 controller라는 명시를 어노테이션을 통해서 지정해준 뒤, 루트(/) 주소에 View를 지정하자.package com.example.demo.main.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controller\t\t// Controller 어노테이션을 통해 이제부터 이 java 파일은 Controller 역할을 수행한다.public class MainController {\t@RequestMapping(value=\"/\")\t\t// 컨트롤 할 URL을 지정해준다. URL의 path 부분을 작성해주면 된다.\tpublic String main() {\t\treturn \"main\";\t\t// View의 폴더, 파일 이름을 문자열로 리턴해준다.\t}}주목할 부분은 @Controller, @RequestMapping 어노테이션들과 return 값의 문자열이다. 최신 스프링 프레임워크에서 Controller를 사용하고 싶다면 간단하게 @Controller 어노테이션을 class 위에 명시해주기만 하면 된다. 그리고 URL 라우팅은 메소드 위에 @RequestMapping 어노테이션을 통해 간단히 구현할 수 있다. (class 위에 @RequestMapping 어노테이션을 명시하여 해당 Controller에 전체적으로 공통 Path를 지정해주는 방법도 있다.)그리고 마지막으로 주목해야할 return 문자열은 View의 폴더, 이름등을 지정해주는데 그렇다면 이 View는 어느 위치에 추가하는 것이며, 파일의 기본 폴더나 확장자 등은 스프링에서 자동으로 처리해 주는 것일까?어느정도 자동으로 처리해주는 부분이 있지만, 기본 폴더나 확장자 등 prefix와 suffix는 따로 잡아줘야 한다. 이를 application.properties 라는 설정 파일에서 잡아줄 수 있다. 이 파일은 아래 이미지와 같이 스프링 프로젝트 생성 시 src/main/resources 폴더 안에 자동으로 생성된다.이제 이 properties 파일을 열고 아래 두 줄을 추가한다.spring.mvc.view.prefix=/WEB-INF/jsp/spring.mvc.view.suffix=.jspprefix는 controller에서 return된 문자열의 앞에 붙는 문자이고, suffix는 뒤에 붙는 문자이다. 위의 controller에서 main을 리턴하면 최종적으로 /WEB-INF/jsp/main.jsp 문자열이 완성되는 것이다.하지만 프로젝트 어딜 찾아봐도 WEB-INF 폴더는 보이지 않는데, 이 폴더는 직접 만들어줘야 한다. 그 경로는 src/main/webapp 의 아래인데, main 폴더를 보면 webapp 폴더도 보이지 않는다. 그래서 webapp 폴더도 직접 만들어줘야 한다. (위에서 View의 폴더 지정 등을 어느정도 자동으로 처리해주는 부분이 있다고 했는데 이 webapp 폴더가 그것이다. 기본적으로 내장 톰캣을 사용한 환경이기 때문에 webapp 폴더를 기본 폴더로 간주하고, 이를 prefix 등에 지정할 필요가 없다.)properties 파일에 위 두줄을 추가했으면 이제 jsp 파일을 추가해주자. 아래 이미지처럼 추가해주면 된다.이 jsp 파일의 body안에 Hello world! 라고 작성해보자. 아래 코드를 참조하자.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><!DOCTYPE html><html><head><meta charset=\"EUC-KR\"><title>Insert title here</title></head><body>\tHello world!</body></html>이제 이 페이지를 브라우저에서 띄워보자. STS를 따로 건드리지 않았다면 좌측 하단에 Boot Dashboard가 있다. (만약 없다면 위에 Window > Show View > Other를 클릭하여 Boot Dashboard를 누르면 된다.)이 Boot Dashboard를 보면 local 항목이 있을텐데 그 안에 내 프로젝트가 있다. 내 프로젝트를 선택한 뒤 그 위의 실행 혹은 디버그 버튼을 클릭하여 프로젝트를 실행하자. 아래 이미지를 참조실행이 완료되면 몇번 포트로 실행이 됬는지 콘솔에 출력된다. 아래 이미지 참조포트를 지정하지 않았다면 기본으로 8080포트에 서버가 뜨게 된다. (포트 변경은 본 포스트 아래쪽 참조)이제 사용하는 브라우저에서 http://localhost:8080에 접속해보자. 아래 이미지처럼 Hello world!가 화면에 찍히면 성공!간단하게 Model 추가해보기이제 기본 main.jsp 페이지에서 이름과 나이를 출력하는 간단한 model을 만들어보자. com.example.demo.main.vo 패키지를 추가한 뒤 Person.java 파일을 추가해주자. (vo 패키지는 dto 등으로도 쓸 수 있다. // 더 세세하게 들어가면 dto와 vo의 차이점이 있지만 본 포스팅에서는 그렇게까지 나누지 않을 예정이고 그냥 편의상 vo로 사용)Person.java 안의 내용은 아래처럼 작성해주자.package com.example.demo.main.vo;public class Person {\tprivate String name = \"Simple\";\tprivate int age = 20;\t\tpublic String getName() {\t\treturn name;\t}\t\tpublic int getAge() {\t\treturn age;\t}}이제 jsp에서 name과 age를 출력해보자.(지금은 간단한 구현을 위해 속성에 값을 직접 할당했지만 실 개발에선 값을 직접 할당하지 않고 거의 대부분의 경우에 Setter를 통해 값을 지정하고 조회한다. 추가로 Lombok등을 이용하여 Getter, Setter의 코드를 줄이는 등의 방법도 있다.)다시 MainController.java 파일을 열어서 아래와 같이 코드를 수정해주자.package com.example.demo.main.controller;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;import com.example.demo.main.vo.Person;// Controller 어노테이션을 통해 이제부터 이 java 파일은 Controller 역할을 수행한다.@Controllerpublic class MainController {\t@RequestMapping(value=\"/\")\t\t// URL의 path 부분을 작성해주면 된다.\tpublic String main(Model model) {\t\tmodel.addAttribute(\"person\", new Person());\t\t// 모델 부여\t\treturn \"main\";\t\t// View의 폴더, 파일 이름을 문자열로 리턴해준다.\t}}그 다음 main.jsp 파일을 열어서 아래와 같이 수정해주자.<%@ page language=\"java\" contentType=\"text/html; charset=EUC-KR\" pageEncoding=\"EUC-KR\"%><!DOCTYPE html><html><head><meta charset=\"EUC-KR\"><title>Insert title here</title></head><body>\tHello world!<br/>\tMy name is ${person.name}<br/>\tI'm ${person.age} years old<br/></body></html>그 다음 서버를 재시작하여 브라우저에 아래와 같이 잘 나오는 지 확인해보자.RestController 사용해보기요즘은 HTTP 프로토콜을 이용한 API 사용에 Restful API를 많이 사용한다. 스프링에서도 이 Restful API 개발을 지원하는데 이때 쓰는 것이 @RestController 어노테이션이다. 위에서 작성한 Person 객체를 JSON 형태로 응답해주는 간단한 RestController를 만들어보자.일단 com.example.demo.main.controller 패키지 안에 MainRestController.java 파일을 추가한다.그 다음 MainRestController 안에 아래와 같이 작성해주자.package com.example.demo.main.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import com.example.demo.main.vo.Person;// RestController 어노테이션을 통해 이제부터 이 java 파일은 RestController 역할을 수행한다.@RestControllerpublic class MainRestController {\t@RequestMapping(value=\"/api\", method=RequestMethod.GET)\t// URL의 path와 HTTP method를 작성한다.\tpublic Person getPerson() {\t\treturn new Person();\t// 객체형으로 응답하면 JSON 형태로 변환된다.\t}}그 다음 http://localhost:8080/api 로 접속하여 Person 객체를 제대로 반환해주는 지 확인한다.추가 설정: port 변경기본으로 사용되는 8080포트가 마음에 안들거나, 다른 포트를 사용해야 될 경우가 빈번하다. 그럴때는 application.properties파일에 설정을 추가해주면 된다. 아래와 같이 설정을 추가해보자.server.port=8081그 다음 서버를 재시작한 뒤 http://localhost:8081로 접속하여 제대로 접속이 되는 지 확인해보자.추가 설정: 코드 수정 시 서버 자동 재시작 (reload, hot load)서버 코드를 수정하게 되면 java 부분은 당연히 재시작을 해야 반영이 되지만, jsp나 정적인 js, image 등등을 수정했을 때도 서버를 재시작해줘야 하는 번거로움이 있다. 이 번거로움을 해결하기 위해 스프링에선 devtools 라는 모듈을 제공하는데, 서버의 코드가 수정되면 자동으로 재시작 및 리로드를 해주는 모듈이다. (java 수정시엔 자동 재시작, jsp나 정적 파일 수정 시엔 재시작 없이 자동 reload)해당 모듈은 pom.xml에 아래 코드를 추가하면 사용할 수 있다.<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional></dependency>모듈 적용을 위해 수동으로 서버 재시작 후, 코드를 수정해보자. 자동으로 서버가 재시작되거나 jsp 수정 후 브라우저 새로고침 시에 수정내용이 반영되면 모듈이 반영된 것이다.",
"url": "/start-spring"
}
,
"google-map-react3": {
"title": "Google Map API React에서 사용하기 (3)",
"author": "Simplehan",
"category": "",
"content": "1. 추가 응용기존 사용 했던 Map 에서 initialCenter 에는 기본적으로 출력해줄 중앙 좌표값을 설정할수 있습니다.기본적으로 {lat : number, lng: number} 형태의 데이터가 필요하며,추후 center 포지션을 바꾸고자 할때는 setCenter() 함수의 사용시 유동적으로 변경 가능합니다.위 설명으로 간단한 예제를 만들어보면.. onChangePosition = (lat, lng) => { let map = this.mapRef.map; let position = new google.maps.LatLng(lat, lng); map.setCenter(position); } initDefaultPosition = () => { let defaultPosition = {lat:0 lng:0}; this.setState({ initPos = defaultPosition }); }이런식으로 간단한 함수를 만들어서 기본 중앙 좌표값 설정 및특정 좌표로 중앙값 이동이 가능합니다.추가적으로 좌표와 좌표간의 거리를 알고자한다면((ACOS( SIN( data.x * PI() / 180) * SIN(data2.x * PI() / 180) + COS( data.x * PI() / 180) * COS(data2.x * PI() / 180) * COS(( data.y - data2.y) * PI() / 180)) * 180 / PI()) * 60 * 1.1515)을 통해 data와 data2의 거리를 mile로 표현 할 수 있습니다.",
"url": "/google-map-react3"
}
,
"instagram-feed": {
"title": "인스타그램 최근 이미지 피드하기",
"author": "Simplehan",
"category": "",
"content": "1. 인스타그램 피드하기시작하기에 앞서, 인스타그램의 Basic Display API 지원 정책 변경으로10월 15일부터 신규 Client 등록 및 허가가 중단되어다른 방법을 알아보다가 알게된 방법입니다.사용법은 매우 간단합니다.먼저 Lightwidget (https://lightwidget.com/) 사이트에 접속하여인스타그램 계정으로 로그인한 후 액세스 권한을 받아옵니다.그 후 아래와 같이 보이는 페이지에서 원하는 값을 설정 합니다.아래로 내려 GetCode!를 누르게 되면,사용할수있는 HTML iframe 태그를 주게 됩니다.",
"url": "/instagram-feed"
}
,
"ble": {
"title": "안드로이드 BLE 통신",
"author": "Simplehan",
"category": "",
"content": "1. 블루투스 권한 설정안드로이드에서 BLE 통신을 사용하기 위해서는APP 이 특정 권한들을 획득해야 한다.아래와 같이 AndroidManifast.xml 파일에 권한 코드를 추가한다. <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/> <uses-permission android:name=\"android.permission.BLUETOOTH\"/> <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>BLE 통신을 위해 BLUETOOTH, BLUETOOTH_ADMIN 권한 외에ACCESS_COARSE_LOCATION라는 위치 권한도 필요하다.BLE 장치가 위치 정보를 제공하는 것이 많기 때문에구글에서 위치 권한도 요청하기로 했다고 한다.안드로이드 마시멜로 버전 이상부터는사용자에게 요청하여 권한을 획득해야 한다. // OS 버전이 마시멜로 이상이라면 위치 권한이 있는지 점검 후 없다면 사용자에게 위치 권한을 요청 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 위치 권한이 있는지 점검 // 없다면 -1 반환 int permission = checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION); // 권한이 없을 때 if (permission == PackageManager.PERMISSION_DENIED) { String[] permissions = new String[1]; permissions[0] = android.Manifest.permission.ACCESS_COARSE_LOCATION; // 사용자에게 요청할 권한 requestPermissions(permissions, LOCATION_PERMISSION_REQUEST_CODE); // 사용자에게 권한 요청 } // 권한이 있을 때 else { // 변수 값을 true 로 변경 isPermissionAllowed = true; } }권한을 요청한 후 사용자의 응답에 따라처리가 가능하도록 메서드를 추가한다.본 예제는 권한을 득하면 바로 주변의BLE 기기를 스캐닝 하도록 구성되어 있다. // 사용자에게 권한을 요청한 결과가 들어오는 메소드 @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { // 위치 권한 요청의 응답값인지 체크 if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) { // 권한을 획득 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // BLE 스캐닝 scanLeDevice(true); } // 권한 획득 실패 else { Toast.makeText(this, R.string.error_permission_denied, Toast.LENGTH_SHORT); finish(); } } }2. BLE SCANBLE 스캐닝 시 배터리 소모가 극심하기 때문에로직을 구현할 때 두가지 권고사항이 있다.1.원하는 기기 발견 시, 스캐닝 중지2.스캐닝을 루프시키지 말 것.위의 두 가지 사항에 따라 설정한 시간이지난 후 스캐닝 후 중지되도록 구현해야 한다. private void scanLeDevice(final boolean enable) { if (enable) { // SCAN_PERIOD 값만큼 시간이 지나면 스캐닝 중지 mHandler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; stopScanBLE(); invalidateOptionsMenu(); } }, SCAN_PERIOD); mScanning = true; startScanBLE(); } else { mScanning = false; stopScanBLE(); } invalidateOptionsMenu(); } // BLE 스캔시작 private void startScanBLE(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback); } else { mBluetoothAdapter.startLeScan(mLeScanCallback); } } // BLE 스캔중지 private void stopScanBLE(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback); } else { mBluetoothAdapter.stopLeScan(mLeScanCallback); } }BLE 기기가 스캔되면 콜백함수가 호출된다.여기서 찾은 기기를 리스트 객체에 추가하고, 화면을 갱신한다. // BLE 기기가 스캔되면 호출. 롤리팝 이하 버전 private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { runOnUiThread(new Runnable() { @Override public void run() { mLeDeviceListAdapter.addDevice(device); mLeDeviceListAdapter.notifyDataSetChanged(); } }); } }; // BLE 기기가 스캔되면 호출. 롤리팝 이상 버전 private ScanCallback mScanCallback = new ScanCallback() { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); BluetoothDevice device = result.getDevice(); mLeDeviceListAdapter.addDevice(device); mLeDeviceListAdapter.notifyDataSetChanged(); } };3. BLE CONNECT검색된 BLE 리스트에서 특정 기기를 선택하면mBluetoothLeService 의 connect 메소드를호출하여 기기에 연결을 시도한다. private BluetoothGatt mBluetoothGatt; private BluetoothGattCharacteristic mReadCharacteristic = null; private BluetoothGattCharacteristic mWriteCharateristic = null; private final String SERVICE = \"0000fff0-0000-1000-8000-00805f9b34fb\"; private final String WRITE_UUID = \"0000fff1-0000-1000-8000-00805f9b34fb\"; private final String READ_UUID = \"0000fff2-0000-1000-8000-00805f9b34fb\"; private boolean findGattServices() { List<BluetoothGattService> gattServices = mBluetoothGatt.getServices(); if (gattServices == null) return false; mReadCharacteristic = null; mWriteCharateristic = null; for (BluetoothGattService gattService : gattServices){ HashMap<String, String> currentServiceData = new HashMap<String, String>(); if(gattService.getUuid().toString().equals(SERVICE)){ List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); // Loops through available Characteristics. for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics){ if(gattCharacteristic.getUuid().toString().equals(READ_UUID)){ final int charaProp = gattCharacteristic.getProperties(); if((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0){ try{ mReadCharacteristic = gattCharacteristic; List<BluetoothGattDescriptor> list = mReadCharacteristic.getDescriptors(); Log.d(TAG, \"read characteristic found : \" + charaProp); mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); //리시버 설정 BluetoothGattDescriptor descriptor = mReadCharacteristic.getDescriptor(UUID.fromString(\"00002902-0000-1000-8000-00805f9b34fb\")); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } catch (Exception e){ e.printStackTrace(); return false; } } else{ Log.d(TAG, \"read characteristic prop is invalid : \" + charaProp); } } else if(gattCharacteristic.getUuid().toString().equalsIgnoreCase(WRITE_UUID)){ final int charaProp = gattCharacteristic.getProperties(); if((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0){ Log.d(TAG, \"write characteristic found : \" + charaProp); mWriteCharateristic = gattCharacteristic; } else{ Log.d(TAG, \"write characteristic prop is invalid : \" + charaProp); } } } } } return true; }BLE 기기는 특정 기능을 보유하고 있는 특성(Characteristic)과그 특성들의 집합인 서비스(Service)로 구성되어 있다.기기의 데이터를 읽고 쓰는 작업을 할 때도 이 특성들을 사용한다.기기와 연결을 성공하면 해당 기기의 서비스와 특성들을 찾아사용할수 있도록 등록해야 한다.특성의 등록이 완료되면 기기 연결의 성공여부를onConnectionStateChange() 메서드에서 받을 수 있다.4. BLE 데이터 통신연결이 성공하면 기기와 데이터를 주고받을 수 있다.기기로 전송할 Command를 가공하여 실행하면 된다. public void writeData(byte[] data){ try{ sendData = null; boolean result = false; if(mBluetoothGatt.connect()) { if(mWriteCharateristic == null){ Log.i(TAG, \"Write gatt characteristic is null\"); } else if(mReadCharacteristic == null){ Log.i(TAG, \"Read gatt characteristic is null\"); } else { int dataLen = data.length; String sendDataG = \"\"; for(int i=0; i<data.length; i++){ sendDataG += String.format(\"%02x\", data[i]); } System.out.println(\"BLE Command 데이터 : \" + sendDataG); sendData = data; Handler hd = new Handler(); hd.postDelayed(new Runnable() { @Override public void run() { mWriteCharateristic.setValue(sendData); if(mWriteCharateristic == null){ Log.d(\"Ble UUID가 없습니다.\", \"\"); mBluetoothGatt.disconnect(); }else { mBluetoothGatt.writeCharacteristic(mWriteCharateristic); } } },5); } } else { Log.i(TAG, \"Bluetooth gatt is not connected\"); } } catch (Exception e) { Log.e(\"\", e.toString()); } }기기에 Command를 전송하였을때 Response 데이터가 있다면응답은 onCharacteristicChanged() 메서드로 콜백된다.private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { . . . . @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { try { byte[] readByte = characteristic.getValue(); if(mBlelistner != null) mBlelistner.recvData(readByte); // 기기 응답을 받는 리스너 } catch(Exception e) { Log.d(TAG, e.toString()); } }}getValue() 값이 기기의 Response 데이터이다.응답을 처리할 리스너를 등록하여 데이터를 처리한다.",
"url": "/ble"
}
,
"google-map-react2": {
"title": "Google Map API React에서 사용하기 (2)",
"author": "Simplehan",
"category": "",
"content": "1. 구글 맵 언어 설정node_modules/google-maps-react/dist/GoogleApiComponent.js 내부의 defaultCreateCache에서 Language값 설정 가능합니다. var language = options.language || 'ko'; //en은 영어입니다.2. 마커의 배열 나눠 출력확대의 줌 값이 바뀔때마다 감지하여,출력할 배열을 바꿔서 출력이 가능합니다.기존에 썼던 마커를 그려 렌더링 해주는 함수를 활용해보면, displayMarkers = (i) => { if (i<=9) { return this.state.list5.map((store, index) => { return ( <Marker key={index} icon={\"/images/logo192.png\"} position= // visible = {this.state.zoomIndex < 15 ? false : true} onClick={() => this.visibleInfoWindow(index)} /> ); }); } else if (i<=11) { return this.state.list4.map((store, index) => { return ( <Marker key={index} position= // visible = {this.state.zoomIndex < 15 ? false : true} onClick={() => this.visibleInfoWindow(index)} /> ); }); }이런식으로 i값을 zoom 값으로 받아온 후,index에 따라 다른 배열을 뿌려주어확대에 따라 보여주는 마커 배열을 바꿀 수 있습니다.3. 마커 이미지 변경다음으론 마커의 아이콘를 변경하는 속성인icon에 대해 알아보겠습니다. <Marker key={index} icon={\"https://developers.google.com/maps/documentation/javascript/images/custom-marker.png\"} position= onClick={() => this.visibleInfoWindow(index)} />이런식으로 마커의 icon에 해당하는 이미지 src를 넣어주면다음과 같이 출력됩니다. ",
"url": "/google-map-react2"
}
,
"google-map-react": {
"title": "Google Map API React에서 사용하기",
"author": "Simplehan",
"category": "",
"content": "1. 시작하기지도를 사용해야하는 프로젝트가 있어서,국내뿐아닌 해외도 보기 쉬운 지도 관련 API를 찾다보니Google maps보다 더 좋은것을 찾기 힘들어해당 API와 React를 같이 사용하여 작업해봤습니다.저와 같은 분들을 위해 React로 Google Map API를 사용하는 방법을작성 해 보겠습니다.먼저 API를 사용하기 위해 KEY값이 필요하므로Google Maps Plat form 사이트에 접속하여 KEY값을 생성합니다.사이트에 접속 하여 다음과 같이 진행하여 키를 생성합니다.(설정이 따로 되어있지않으면, 결제 페이지가 나옵니다) 그 후 터미널에 다음과 같이 입력하여React로 프로젝트를 생성후 Google Map API를 설치합니다. npm init react-app AppName npm install --save google-maps-react yarn install그 후 컨테이너에서 google-maps-react를 import합니다. import { Map, GoogleApiWrapper, Marker } from 'google-maps-react';렌더링할 내용은 다음과 같습니다. render() { const mapStyles = { width: '100%', height: '100%', }; return ( <Fragment> <Map google={this.props.google} zoom={18} style={mapStyles} initialCenter= > </Map> </Fragment> ); }또한 API를 사용하기 위한 KEY값을 설정하기 위해export에 받아온 값을 저장합니다. export default GoogleApiWrapper({ apiKey: 'YOUR GOOGLE MAPS API KEY 입력' })(YourCntName);2. 사용 방법 HandlerEvnet기본적으로 핸들러들의 형태는on(HandlerEventType) 의 형태로 되어있으며,다음과 같이 이벤트를 추가 할수 있습니다. <Map google={this.props.google} onClick ={this.onEventChecker} />파라미터는 총 3개입니다.e는 html elementaug는 Map에 설정된 데이터 값geo는 좌표값을 받을수 있는 함수를 넘겨줍니다. onEventChecker = (e, aug, geo) => { console.log(e); console.log(aug); console.log(geo); }이러한 핸들러 이벤트를 활용하여지도에 마커를 추가하는 함수를 생성해 봅시다. addMarkers = async (e, aug, geoData) => { // console.log(aug); const {stores} = this.state; let stateData = stores; let latLng; latLng = {latitude : geoData.latLng.lat(), longitude : geoData.latLng.lng()}; stateData.push(latLng); await this.setState({ stores: stateData }); }그리고 배열을 그려주는 함수를 생성하여state의 배열이 추가될때마다 마커를 추가합니다. displayMarkers = () => { return this.state.stores.map((store, index) => { return <Marker key={index} id={index} position= label={store.title} onClick={() => this.removeMarkers(index)} /> }) } render() { const mapStyles = { width: '100%', height: '100%', }; return ( <Fragment> <Map google={this.props.google} zoom={18} style={mapStyles} initialCenter= onClick={this.addMarkers} > {this.displayMarkers()} </Map> </Fragment> ); }마커를 그려주는 함수를 잘 보시면,마커를 클릭시 해당 마커를 지울수 있도록 생성된 걸확인 할 수 있습니다.해당 함수의 코드는 다음과 같습니다. removeMarkers = async (i) => { const {stores} = this.state; let stateData = stores; stateData.splice(i,1); await this.setState({ stores: stateData }) }여기까지 작성한 예제 코드를 실행 한 후화면은 아래와 같습니다. 해당 화면에서 클릭 이벤트를 실행시.. 주소 검색먼저 검색을 할 input과 button을 렌더링합니다. 주소/건물 : <input value={this.state.searchText} onChange={(e) => this.onChangeInput(e)} /> <button onClick={()=>this.onSearchEvent()}>검색</button>그 후 state에 검색할 데이터를가지고 있는 searchText를 만들어주며,검색 함수를 생성합니다. onChangeInput = (e) => { this.setState({ searchText: e.target.value }) } onSearchEvent = () => { const {google} = this.props; const {searchText} = this.state let geocoder = new google.maps.Geocoder(); geocoder.geocode({ address: searchText, region: 'ko' }, function (results, status) { if (status === google.maps.GeocoderStatus.OK) { console.log(\"RESULT OK::통신 성공\"); console.log(results); } else if (status === google.maps.GeocoderStatus.ERROR) { console.log(\"Error::server 통신 에러\"); } else if (status === google.maps.GeocoderStatus.INVALID_REQUEST) { console.log(\"Invalid request::검색어없음\"); } else if (status === google.maps.GeocoderStatus.OVER_QUERY_LIMIT) { console.log(\"Over query limit::너무 잦은 통신\"); } else if (status === google.maps.GeocoderStatus.REQUEST_DENIED) { console.log(\"Geo code denied::geoCode 이용불가 API KEY 확인\"); } else if (status === google.maps.GeocoderStatus.UNKNOWN_ERROR) { console.log(\"Unknown error::알수없는 에러 발생\"); } else if (status === google.maps.GeocoderStatus.ZERO_RESULTS) { console.log(\"Zero result::찾는 데이터와 일치하는 데이터 존재하지않음.\"); } else { console.log(\"unknown status error..\"); } }) }다음은 해당 함수를 통해부평구를 검색했을때 콜백받은 result의 값입니다. formatted_address 값이 주소명이며,좌표를 가져올 수 있는 함수 또한 포함 되어 있음을확인 할 수 있습니다. InfoWindow좌표에 설명하기 위한 라벨이아닌,메모같은 정보창을 표시할 수 있는InfoWindow가 존재합니다.먼저 InfoWindow를 import 합니다. import { Map, GoogleApiWrapper, Marker, InfoWindow } from 'google-maps-react';렌더링 될 때엘리먼트로 넣어주면 사용이 가능합니다. <InfoWindow visible= {Boolean} position= content= {String} onClose= {function} > </InfoWindow> // 예제에 사용되는 매개변수와 타입입니다.onClose 같은 경우에는 기본 함수가 있으며visible의 값을 false 로 바꿔줍니다.이번에 사용할 방법은 Marker를 클릭시infoWindow를 활성화 시키며,close시 비활성화 시킬수 있도록 만들어 보겠습니다.먼저 Marker의 onClick과InfoWindow의 onClose에 들어갈 함수를 생성합니다. visibleInfoWindow = async (i) => { const {stores} = this.state; let stateData = stores; stateData[i].bool = !stateData[i].bool await this.setState({ stores: stateData }) }기존에 state의 저장 했던stores의 키값에는 visible에 들어갈boolean 값이 없으므로 추가해줍니다. const latLng = {latitude:lat, longitude:lng, title: address, bool: false};Marker를 클릭시 InfoWindow가 보여야하므로onClick을 바꾸며,InfoWindow의 경우Marker 를 그리는 함수와 동일 한 방식으로출력할 수 있도록 그리는 함수를 생성합니다. displayMarkers = () => { return this.state.stores.map((store, index) => { return ( <Marker key={index} position= onClick={() => this.visibleInfoWindow(index)} /> ); }) } displayInfoWindows = () => { return this.state.stores.map((store, index) => { return ( <InfoWindow key={index} visible={store.bool} position= content={store.title} onClose={()=>this.visibleInfoWindow(index)} > </InfoWindow> ); }) }그 후 렌더링 되는 Map 안에다음과 같이 추가합니다 {this.displayInfoWindows()}예제 코드 실행시 아래와 같은 그림으로 실행됩니다. 해당 마커를 클릭하면.. 이렇게 InfoWindow가 출력되는 것을 확인할수 있습니다.3. 확대/축소 이벤트이번에는 ref를 활용하여 Map의 데이터를 가져온 후마우스 휠의 이벤트를 감지하여state의 값으로 저장하도록 만들어 보겠습니다.먼저 이벤트를 감지할 수 있도록 세팅합니다. componentDidMount = () => { window.addEventListener(\"DOMMouseScrol\", this.onWheelEvent, false); window.onmousewheel = document.onmousewheel = this.onWheelEvent; }그 후 마우스가 감지되었을때 사용할 이벤트인onWheelEvent 를 생성합니다. onWheelEvent = (event) =>{ var delta = 0; if (!event) event = window.event; if (event.wheelDelta) { delta = event.wheelDelta/120; if (window.opera) delta = -delta; } else if (event.detail) delta = -event.detail/3; if (delta) this.onWheelHandler(delta); }해당 이벤트가 감지되었을때 처리된 값을받아줄 핸들러를 생성합니다. onWheelHandler = (delta) => { let map = this.mapRef.map; console.log(\"lat::\"+map.center.lat()); console.log(\"lng::\"+map.center.lng()); console.log(\"zoom::\"+map.zoom); if (delta < 0) { console.log(\"wheel down\"); } else { console.log(\"wheel up\"); }; this.setState({ zoomIndex: map.zoom }); }참고로 리액트의 ref 사용법은다음과 같이 사용됩니다 ref={ref => this.mapRef = ref}이런식으로 속성값을 태그에 넣어주시면 ref값이 저장되어해당 엘리먼트를 확인 가능합니다.이경우 결과 값은 다음 사진과 같습니다. 다만 해당 방식으로 처리할 경우엔 스마트폰의 확대 및 축소감지를 하지 못하게 되어 원래사용되던 onZoomChanged 함수를 사용 하였으나,작동되지않아 해당 방식으로 web에서나마처리되도록 만들어 보았습니다.",
"url": "/google-map-react"
}
,
"react2": {
"title": "React 튜토리얼 2",
"author": "Simplehan",
"category": "",
"content": "1. 시작하기시작 코드를 가지고 시작해봅시다. class Square extends React.Component { render() { return ( <button className=\"square\"> {/* TODO */} </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { const status = 'Next player: X'; return ( <div> <div className=\"status\">{status}</div> <div className=\"board-row\"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className=\"board-row\"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className=\"board-row\"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { render() { return ( <div className=\"game\"> <div className=\"game-board\"> <Board /> </div> <div className=\"game-info\"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') );이 코드는 우리가 구현할 틱택토 게임의 틀을 가지고 있습니다. 필요한 스타일들을 준비해두었기 때문에 JavaScript만 신경쓰면 됩니다.세 가지 컴포넌트로 구성되어 있습니다:-Square -Board -GameSquare 컴포넌트는 하나의 <button>을 랜더링합니다. Board 컴포넌트는 9개의 사각형을 랜더링합니다. Game 컴포넌트는 나중에 우리가 채워 넣어야 할 공백이 있는 하나의 보드를 랜더링합니다. 지금 이 컴포넌트들은 아무런 동작도 하지 않습니다.props를 통해 데이터 전달하기 본격적으로 시작하기 위해 데이터를 Board 컴포넌트에서 Square 컴포넌트로 전달해봅시다.Board의 renderSquare 메서드에서 Square 컴포넌트 prop에 value 값을 전달하도록 코드를 변경해주세요 class Board extends React.Component { renderSquare(i) { return <Square value={i} />; }value 값을 보여주기 위해 Square 컴포넌트의 render 메서드 안의 코드 {/* TODO */}를 {this.props.value}로 변경해주세요 class Square extends React.Component { render() { return ( <button className=\"square\"> {this.props.value} </button> ); } }대화형 컴포넌트 클릭 시 “X”로 채워지는 Square 컴포넌트를 만들어봅시다. Square의 render() 함수에서 반환된 버튼 태그를 다음과 같이 변경해주세요 class Square extends React.Component { render() { return ( <button className=\"square\" onClick={() => alert('click')}> {this.props.value} </button> ); } }이제 사각형을 클릭하면 브라우저에서 알럿창이 뜨는걸 확인할 수 있습니다.새로운 JavaScript 문법인 화살표 함수를 사용하였습니다. onClick prop에 함수를 전달하였습니다. onClick={alert(‘click’)} 코드를 작성하고 버튼을 클릭하면 알럿창 대신 경고가 뜨게됩니다.React 컴포넌트는 생성자에서 this.state를 설정하여 state를 가질 수 있고 state는 컴포넌트에서 private으로 간주되어야 합니다. 사각형의 현재 value 값을 상태에 저장하고 클릭할 때 바뀌도록 만들어봅시다.먼저 state를 초기화하기 위해 클래스에 생성자를 추가해주세요 class Square extends React.Component { constructor(props) { super(props); this.state = { value: null, }; } render() { return ( <button className=\"square\" onClick={() => alert('click')}> {this.props.value} </button> ); } }JavaScript 클래스에서 서브클래스의 생성자를 정의할 때 super(); 메서드를 명시적으로 호출해줘야 합니다.이제 Square의 render 메서드에서 현재 상태의 value 값을 표시하고 클릭할 때 바뀌도록 수정해주세요.<button> 태그 안의 this.props.value 를 this.state.value로 변경해주세요. () => alert() 이벤트 핸들러를 () => this.setState({value: ‘X’})로 변경해주세요. <button> 태그는 다음과 같습니다 class Square extends React.Component { constructor(props) { super(props); this.state = { value: null, }; } render() { return ( <button className=\"square\" onClick={() => this.setState({value: 'X'})}> {this.state.value} </button> ); } }this.setState가 호출될 때마다 컴포넌트가 업데이트되므로 업데이트된 state가 전달되어 React가 이를 병합하고 하위 컴포넌트와 함께 다시 랜더링합니다. 컴포넌트가 랜더링될 때 this.state.value는 ‘X’가 되어 그리드 안에 X가 보이게 됩니다.이제 사각형을 클릭하면 그 안에 X가 표시됩니다.",
"url": "/react2"
}
,
"react1": {
"title": "React 튜토리얼",
"author": "Simplehan",
"category": "",
"content": "1. 개요React는 유저 인터페이스 구현을 위한 Javascript 라이브러리중 하나입니다.React는 여러 종류의 컴포넌트들을 가지고 있지만, 우리는 React.Component의 서브 클래스를 사용하여 시작 할 것입니다. class HomeCmp extends React.Component { render() { return ( <div className=\"homeWrp\"> <h1>Web page list for {this.props.name}</h1> <ul> <li>Instagram</li> <li>Naver</li> <li>Google</li> </ul> </div> ); } }XML과 비슷한 태그들을 사용하게 되는데, 작성한 컴포넌트는 React에게 무엇을 렌더링 하고싶은지 알려줍니다. 그 후 React는 데이터가 변경될때, 올바른 컴포넌트들을 업테이트 하고, 렌더링합니다.여기에서 HomeCmp는 React 컴포넌트 클래스 혹은 React 컴포넌트 타입입니다. 하나의 컴포넌트는 props라 불리는 파라미터를 사용하며, render 메서드를 통해 표시할 뷰 계층 구조를 반환합니다.render 메서든느 렌더링하길 원하는 내용을 반환하면 REact는 그 내용을 가져와 스크린에 렌더링 합니다. 특히 render는 React 엘리먼트를 반환하는데 이는 렌더링 할 부분에 대한 간단한 내용입니다.대부분의 Rect 개발자들은 이 구조를 더 쉽게 작성할 수 있게 해주는 JSX라는 문법을 사용합니다. <div/> 라 쓰면 빌드시 React.createElement(‘div’)로 변환 되는데, 위의 코드는 아래의 코드와 동일합니다. return React.createElement(\"div\", { className: \"homeWrp\" }, React.createElement(\"h1\", null, \"Web page list for\", props.name), React.createElement(\"ul\", null, React.createElement(\"li\", null, \"Instagram\"), React.createElement(\"li\", null, \"Naver\"), React.createElement(\"li\", null, \"Google\") ) );createdElement()에 대해 더 많은 내용이 궁금하면 API reference 에 자세한 설명이 있습니다.튜토리얼에서는, createdElement()를 직접적으로 사용하지 않고 JSX를 사용하겠습니다.JSX에서는 중괄호 안에 Javascript 문법을 사용할수 있습니다. 각 React 엘리먼트는 변수에 저장하거나 프로그램 여기저기에 전달할 수 있는 실제 Javascript 객체입니다.HomeCmp 컴포넌트는 내장된 DOM 컴포넌트만 랜더링하지만 <HomeCmp /> 코드를 작성하여 커스텀 React 컴포넌트를 쉽게 구성할 수 있습니다. 각 컴포넌트는 캡슐화되어 독립적으로 동작할 수 있기 때문에 간단한 컴포넌트들로 복잡한 UI를 구현할 수 있습니다.",
"url": "/react1"
}
,
"ios-use-cordova": {
"title": "코르도바 활용 블루투스 프로젝트 생성",
"author": "Simplehan",
"category": "",
"content": "1. 코르도바 개발환경 설정Cordova 개발환경 설정시 기본적으로 필요한 프로그램 목록입니다. -node.js 다운로드 node는 특별한 사유가없다면 LTS버전으로 설치합니다.\t-cordova npm 설치 후 터미널을 열고 sudo npm -g install cordova설치 완료 후, cordova -v를 통해 코르도바 버전을 확인합니다. (8.1.2 버전 기준으로 작성하였으므로 구동되지 않을시 해당 버전의 코르도바로 설치합니다) 그 후 ios 시뮬레이터를 호출할 수 있는 ios-sim을 설치합니다. sudo npm -g install ios-sim이후 새로운 코르도바 프로젝트를 만들고자 할 경우 프로젝트를 생성하고자하는 경로에서 ios플랫폼이 있는 코르도바 프로젝트를 생성할 수 있습니다. cordova create example com.example.test “TestExample” cd example cordova platform add ios cordova prepare // or cordova build필요한 plugin의 설치 및 삭제는 다음 명령어로 가능합니다. cordova plugin add (삭제는 remove) [Plugin Name]2. WebView와 내부 메소드 연동방법 (javascript 호출)내부 plugines 폴더 내부에 new file을 통해 objectve-c 파일을 생성합니다. (Subclass of는 CDVPlugin 을 참조하도록 설정후 원하는 class name을 설정후 objectve-c를 선택합니다.) 그렇다면 설정한 classname.h 와 classname.m 파일이 생성 될 것이며, 우선 h 파일을 열어 코드를 수정합니다. (MGPluginEcho 으로 생성됨) #Import <Cordova/CDV.h> @Interface MGPluginEcho: CDVPlugin -(void)echo:(CDVInvokedUrlCommand *)command; @end다음은 m파일입니다. #Import “MGPluginEcho.h” @Implementation MGPluginEcho -(void)echo: (CDVInvokedUrlCommand *)command { NSString* message = [command.arguments objectAtindex:0]; NSLog(@”message : %@”, messaage); [[[UIAlertView alloc] InitWithTitle:@”네이티브팝업창” message:message delegate: nil CancelButtonTitle:@”취소” OtherButtonTitles:@”확인”,nil]show]; } @End여기까지가 native에서 생성가능 한 코드이며, 이후 javascript에서 사용 가능하도록 연동해야합니다. 우선 config.xml 내부에 추가하여 연동하는 방법입니다. <feature name=\"MGPluginEcho\">\t <param name=\"ios-package\" value=\"MGPluginEcho\" /> </feature>다음은 javascript 입니다. function MGPluginEcho () {} MGPlugin.prototype.echo = function (msg) { Cordova.exec(null, null, 'MGPluginEcho', 'echo', [msg]); } var runEcho = function () { var pluginEcho = new MGPluginEcho(); pluginEcho.echo('javascript/native 연동 html에서 보낸 내용 나타내기'); }3. 블루투스 연결 방법간편한 방법으로 cordova-plugin-ble-central 하여 사용할 수 있으며, 작동원리는 위의 Native / Javascript 연동과 같습니다. 터미널에서 설치시 plugin에 ble-cental-plugin.m /h 파일이 생성됩니다. 콜백함수는 다음과 같습니다. - (void)scan:(CDVInvokedUrlCommand *)command; - (void)startScan:(CDVInvokedUrlCommand *)command; - (void)startScanWithOptions:(CDVInvokedUrlCommand *)command; - (void)stopScan:(CDVInvokedUrlCommand *)command; - (void)connect:(CDVInvokedUrlCommand *)command; - (void)disconnect:(CDVInvokedUrlCommand *)command; - (void)read:(CDVInvokedUrlCommand *)command; - (void)write:(CDVInvokedUrlCommand *)command; - (void)writeWithoutResponse:(CDVInvokedUrlCommand *)command; - (void)startNotification:(CDVInvokedUrlCommand *)command; - (void)stopNotification:(CDVInvokedUrlCommand *)command; - (void)isEnabled:(CDVInvokedUrlCommand *)command; - (void)isConnected:(CDVInvokedUrlCommand *)command; - (void)startStateNotifications:(CDVInvokedUrlCommand *)command; - (void)stopStateNotifications:(CDVInvokedUrlCommand *)command; - (void)onReset; - (void)readRSSI:(CDVInvokedUrlCommand *)command;좀더 자세한 사항은 git-hub 를 통해 확인 가능합니다.4. iOS백그라운드 서비스iOS 백그라운드 서비스를 코르도바에서 사용하기위해 우선, 다음과 같은 플러그인을 설치합니다. cordova plugin add cordova-custom-config <platform name=\"ios\"> <config-file parent=\"UIBackgroundModes\" target=\"*-Info.plist\"> <array> <string>bluetooth-central</string> </array> </config-file> </platform>또한 info.plist 에서 UIBackgroundModes가 활성화후, ble 통신 및 backgoround fetch를 활성화 하여 백그라운드 서비스를 정상적으로 작동 할 수 있습니다. 구동 원리의 코드는 appDelegate 이며 코드를 통해 background 패치를 초기화 시켜 주므로 백그라운드 상태에서도 계속 사용 가능해집니다. - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { self.viewController = [[MainViewController alloc] init]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }5. PUSH 개발 방법 (FireBase 연동)우선 cordova fcm 플러그인을 설치합니다. cordova plugin add cordova-plugin-fcm그후 firebase 홈페이지에 접속하여 다음과 같이 진행합니다. 여기서 주의해야 할 부분은, 번들 ID는 프로젝트에서 생성한 번들 ID와 같아야합니다. GoogleService-info.plist 파일을 다운로드 받은 후 프로젝트 내부 폴더로 옮겨줍니다. 전부 설정 후html에서 토큰 설정후 사용할 수 있습니다. (사진과 다르게 현재 프로젝트에서는 resources 폴더 내부에 저장하여 사용중입니다.) 또한 Push 사용을 위해 인증서 업로드가 필요한데 애플 개발자 사이트에서 인증서를 받은후 FCM에 업로드 하여 등록 가능합니다. 그후 네이티브 소스에서 해당 소스를 통해 기본 설정을 할 수 있다고 되어있으나 현재 프로젝트에서는 cordova-fcm 플러그인을 통해 사용중이며 javascript에서 호출되어 사용되고 있습니다. (plugin/FCMPlugin.m) 그 후 Cloud Messaging을 통해 메시지를 전송할 수 있습니다 (특정 토큰 선택 또는 전체 가능) 6. Splash 및 App icon 설정Images.xcassets 폴더 내부의 LaunchImage.launchimage 폴더 내부의 이미지 파일들이 Splash 이미지이며, AppIcon.appiconset 폴더의 내부 파일들이 App icon 이미지입니다. 이미지 파일 수정시 Splash 및 App icon 설정이 가능합니다. 또는 xcode 실행시 Resource 폴더 내부의 images.xcassets 내부로 접근시 해당 이미지를 좀더 편하게 수정 할 수 있도록 그림과 같은 화면이 출력됩니다. 7. 앱에서 필요한 퍼미션 권한 허용, 거부 출력대부분 권한은 이곳에서 설정 가능하며, 오류 발생시 resources 폴더 내부의 plist 파일의 key와 type value가 정상적으로 입력되었는지 확인합니다.퍼미션 종류는 다음 그림과 같습니다.해당 퍼미션을 info.plist에 추가하면 퍼미션 확인 메시지를 출력할수 있습니다이상으로 iOS - Cordova를 활용한 BLE 프로젝트 생성 및 필요한 환경 설정 세팅에 대해 포스트 했습니다.",
"url": "/ios-use-cordova"
}
,
"ios-swift2": {
"title": "[iOS] Swift 기본 (2)",
"author": "Simplehan",
"category": "",
"content": "5. 클래스와 구조체클래스는 class로 정의하고, 구조체는 struct로 정의합니다. class Dog { var name: String? var age: Int? func simpleDescription() -> String { if let name = self.name { return\"🐶 \\(name)\" } else { return\"🐶 No name\" } } } struct Coffee { var name: String? var size: String? func simpleDescription() -> String { if let name = self.name { return\"☕️ \\(name)\" } else { return\"☕️ No name\" } } } var myDog = Dog() myDog.name = \"찡코\" myDog.age = print(myDog.simpleDescription()) // 🐶 찡코 var myCoffee = Coffee() myCoffee.name = \"아메리카노\" myCoffee.size = \"Venti\"print(myCoffee.simpleDescription()) // ☕️ 아메리카노클래스는 상속이 가능합니다. 구조체는 불가능합니다. class Animal { let numberOfLegs = 4 } class Dog: Animal { var name: String? var age: Int? } var myDog = Dog() print(myDog.numberOfLegs) // Animal 클래스로부터 상속받은 값 (4)클래스는 참조Reference하고, 구조체는 복사Copy합니다. var dog1 = Dog() // dog1은 새로 만들어진 Dog()를 참조합니다. var dog2 = dog1 // dog2는 dog1이 참조하는 Dog()를 똑같이 참조합니다. dog1.name = \"찡코\"// dog1의 이름을 바꾸면 Dog()의 이름이 바뀌기 때문에, print(dog2.name) // dog2의 이름을 가져와도 바뀐 이름(\"찡코\")이 출력됩니다. var coffee1 = Coffee() // coffee1은 새로 만들어진 Coffee() 그 자체입니다. var coffee2 = coffee1 // coffee2는 coffee1을 복사한 값 자체입니다. coffee1.name = \"아메리카노\"// coffee1의 이름을 바꿔도 coffee2.name // coffee2는 완전히 별개이기 때문에 이름이 바뀌지 않습니다. (nil)생성자 (Initializer) 클래스와 구조체 모두 생성자를 가지고 있습니다. 생성자에서는 속성의 초깃값을 지정할 수 있습니다. class Dog { var name: String? var age: Int? init() { self.age = 0 } } } struct Coffee { var name: String? var size: String? init() { self.size = \"Tall\" } } 만약 속성이 옵셔널이 아니라면 항상 초깃값을 가져야 합니다. 만약 옵셔널이 아닌 속성이 초깃값을 가지고 있지 않으면 컴파일 에러가 발생합니다. class Dog { var name: String? var age: Int// 컴파일 에러! } stored property 'age' without initial value prevents synthesized initializers속성을 정의할 때 초깃값을 지정해 주는 방법과, class Dog { var name: String? var age: Int = 0// 속성을 정의할 때 초깃값 지정 } 생성자에서 초깃값을 지정해주는 방법이 있습니다. class Dog { var name: String? var age: Int init() { self.age = 0// 생성자에서 초깃값 지정 } } 생성자도 함수와 마찬가지로 파라미터를 받을 수 있습니다. class Dog { var name: String? var age: Int init(name: String?, age: Int) { self.name = name self.age = age } } var myDog = Dog(name: \"찡코\", age: 3) 만약 상속받은 클래스라면 생성자에서 상위 클래스의 생성자를 호출해주어야 합니다. 만약 생성자의 파라미터가 상위 클래스의 파라미터와 같다면, override 키워드를 붙여주어야 합니다. super.init()은 클래스 속성들의 초깃값이 모두 설정 된 후에 해야 합니다. 그리고 나서부터 자기 자신에 대한 self 키워드를 사용할 수 있습니다. class Dog: Animal { var name: String? var age: Int overrideinit() { self.age = 0// 초깃값 설정 super.init() // 상위 클래스 생성자 호출 print(self.simpleDescription()) // 여기서부터 `self` 접근 가능 } func simpleDescription() -> String { if let name = self.name { return\"🐶 \\(name)\" } else { return\"🐶 No name\" } } } 만약, 위 예시 코드를 아래처럼 바꿔서 super.init()을 하기 전에 self에 접근한다면 컴파일 에러가 발생합니다. overrideinit() { self.age = print(self.simpleDescription()) // 컴파일 에러! super.init() } error: use of ‘self’ in method call ‘simpleDescription’ before super.init initializes self deinit은 메모리에서 해제된 직후에 호출됩니다. class Dog { ...deinit { print(\"메모리에서 해제됨\") } } 속성 (Properties) 속성은 크게 두 가지로 나뉩니다. 값을 가지는 속성Stored Property과 계산되는 속성Computed Property 입니다. 쉽게 말하면 속성이 값 자체를 가지고 있는지, 혹은 어떠한 연산을 수행한 뒤 그 결과를 반환하는지의 차이입니다. 우리가 지금까지 정의하고 사용한 name, age와 같은 속성들은 모두 Stored Property입니다. Computed Property는 get, set을 사용해서 정의할 수 있습니다. set에서는 새로 설정될 값을 newValue라는 예약어를 통해 접근할 수 있습니다. struct Hex { var decimal: Int? var hexString: String? { get { if let decimal = self.decimal { returnString(decimal, radix: 16) } else { return nil } } set { if let newValue = newValue { self.decimal = Int(newValue, radix: 16) } else { self.decimal = nil } } } } var hex = Hex() hex.decimal = 10 hex.hexString // \"a\" hex.hexString = \"b\" hex.decimal // 11위 코드에서 hexString은 실제 값을 가지고 있지는 않지만, decimal로부터 값을 받아와 16진수 문자열로 만들어서 반환합니다. decimal은 Stored Property, hexString은 Computed Property입니다. 참고로, get만 정의할 경우에는 get 키워드를 생략할 수 있습니다. 이런 속성을 읽기 전용Read Only이라고 합니다. class Hex { ...var hexCode: String? { if let hex = self.hexString { return \"0x\" + hex } return nil } } get, set과 비슷한 willSet, didSet을 이용하면 속성에 값이 지정되기 직전과 직후에 원하는 코드를 실행할 수 있습니다. struct Hex { var decimal: Int? { willSet { print(\"\\(self.decimal)에서 \\(newValue)로 값이 바뀔 예정입니다.\") } didSet { print(\"\\(oldValue)에서 \\(self.decimal)로 값이 바뀌었습니다.\") } } } 마찬가지로, willSet에서는 새로운 값을 newValue로 얻어올 수 있고, didSet에서는 예전 값을 oldValue라는 예약어를 통해 얻어올 수 있습니다. willSet과 didSet은 일반적으로 어떤 속성의 값이 바뀌었을 때 UI를 업데이트하거나 특정 메서드를 호출하는 등의 역할을 할 때에 사용됩니다.6. 튜플튜플Tuple은 어떠한 값들의 묶음입니다. 배열과 비슷한 개념이지만, 배열과는 다르게 길이가 고정되어 있습니다. 값에 접근할 때에도 [] 대신 .을 사용합니다. var coffeeInfo = (\"아메리카노\", 5100) coffeeInfo.0// 아메리카노 coffeeInfo.1// 5100 coffeeInfo.1 = 5100이 튜플의 파라미터에 이름을 붙일 수도 있습니다. var namedCoffeeInfo = (coffee: \"아메리카노\", price: 5100) namedCoffeeInfo.coffee // 아메리카노 namedCoffeeInfo.price // 5100 namedCoffeeInfo.price = 5100이렇게 보면, 앞서 살펴본 구조체와 비슷한데, 실제로도 간단한 자료형을 만들 때에는 구조체 대신 튜플을 사용하기도 합니다. 튜플의 타입 어노테이션은 이렇게 되어있습니다. var coffeeInfo: (String, Int) var namedCoffeeInfo: (coffee: String, price: Int) 튜플을 조금 응용하면, 아래와 같이 여러 변수에 값을 지정할 수도 있습니다. let (coffee, price) = (\"아메리카노\", 5100) coffee // 아메리카노 price // 5100튜플이 가진 값을 가지고 변수에 값을 지정할 때, 무시하고 싶은 값이 있다면 _ 키워드를 사용해서 할 수 있습니다. 아래 코드에서는 “라떼”라는 첫 번째 값을 무시합니다. let (_, latteSize, lattePrice) = (\"라떼\", \"Venti\", 5600) latteSize // Venti lattePrice // 5600물론, 튜플을 반환하는 함수도 만들 수 있습니다. /// 커피 이름에 맞는 커피 가격 정보를 반환합니다. 일치하는 커피 이름이 없다면 `nil`을 반환합니다.////// - parameter name: 커피 이름////// - returns: 커피 이름과 가격 정보로 구성된 튜플을 반환합니다. func coffeeInfo(for name: String) -> (name: String, price: Int)? { let coffeeInfoList: [(name: String, price: Int)] = [ (\"아메리카노\", 5100), (\"라떼\", 5600), ] for coffeeInfo in coffeeInfoList { if coffeeInfo.name == name { return coffeeInfo } } return nil } coffeeInfo(for: \"아메리카노\")?.price // 5100 coffeeInfo(for: \"에스프레소\")?.price // nil let (_, lattePrice) = coffeeInfo(for: \"라떼\")! lattePrice // 5600 7. Enum열거라는 뜻을 가진 Enumeration에서 따온 용어입니다. 한글로 번역할 때에는 열거형이라는 말을 많이 사용합니다. 1월부터 12월까지를 enum으로 한 번 정의해보겠습니다. enum Month: Int { case january = 1 case february case march case april case may case june case july case august case september case october case november case december func simpleDescription() -> String { Switch self { case .january: return\"1월\" case .february: return\"2월\" case .march: return\"3월\" case .april: return\"4월\" case .may: return\"5월\" case .june: return\"6월\" case .july: return\"7월\" case .august: return\"8월\" case .september: return\"9월\" case .october: return\"10월\" case .november: return\"11월\" case .december: return\"12월\" } } } let december = Month.december print(december.simpleDescription()) // 12월 print(december.rawValue) // 12위 예시에서 작성한 Month는 Int를 원시값Raw Value으로 가지도록 정의되었습니다. 그렇기 때문에 각 케이스들은 1부터 12까지의 값을 가지고 있습니다. rawValue 속성이 바로 그 값을 나타내는데, 반대로 원시값을 가지고 Enum을 만들 수도 있습니다. let october = Month(rawValue: 10) print(october) // Optional(Month.october)Month(rawValue:)의 반환값이 옵셔널인 이유는, Enum에서 정의되지 않은 원시값을 가지고 생성할 경우 nil을 반환하기 때문입니다. Month(rawValue: 13) // nil일반적으로 Enum은 Int만을 원시값으로 가질 수 있다고 생각합니다. 다른 프로그래밍 언어에서는 모두 그렇기 때문입니다. 하지만, Swift의 Enum은 조금 독특합니다. 아래 예시는 String을 원시값으로 가지는 Enum입니다. enum IssueState: String { case open = \"open\"case closed = \"closed\" } 만약 어떤 API의 응답에서 내려주는 state의 값이 open 또는 closed라면, if-else 없이도 IssueState(rawValue:)를 사용해서 Enum을 생성할 수 있습니다. Enum은 원시값을 가지지 않을 수도 있습니다. 원시값을 가져야 할 필요가 없다면 굳이 만들지 않아도 됩니다. enum Spoon { case dirt case bronze case silver case gold func simpleDescription() -> String { switch self { case .dirt: return\"흙수저\" case .bronze: return\"동수저\" case .silver: return\"은수저\" case .gold: return\"금수저\" } } } Enum을 예측할 수 있다면 Enum의 이름을 생략할 수 있습니다. let spoon: Spoon = .gold // 변수에 타입 어노테이션이 있기 때문에 생략 가능 func doSomething(with spoon: Spoon) { /* ... */} doSomething(with: .silver) // 함수 정의에 타입 어노테이션이 있기 때문에 생략 가능연관 값(Associated Values)을 가지는 EnumEnum은 연관 값Associated Values을 가질 수 있습니다. 아래 예시는 어떤 API에 대한 에러를 정의한 것인데요. invalidParameter 케이스는 필드 이름과 메시지를 가지도록 정의되었습니다. enum NetworkError { case invalidParameter(String, String) case timeout } let error: NetworkError = .invalidParameter(\"email\", \"이메일 형식이 올바르지 않습니다.\") 이 값을 꺼내올 수 있는 방법으로는 if-case 또는 switch를 활용하는 방법이 있습니다. if case .invalidParameter(let field, let message) = error { print(field) // email print(message) // 이메일 형식이 올바르지 않습니다. } switch error { case .invalidParameter(let field, let message): print(field) // email print(message) // 이메일 형식이 올바르지 않습니다.default: break } /*응용하기: NetworkError에 message라는 읽기 전용 속성을 추가하고, 에러에 대한 명확한 메시지를 반환하도록 만들어봅시다. 더 나아가서, 있을법한 다른 에러에 대한 경우도 추가해봅시다.*/사실, 옵셔널은 Enum입니다. 실제 구조도 다음과 같습니다. public enum Optional<Wrapped> { case none case some(Wrapped) }옵셔널이 왜 ‘값’과 ‘없는 값’을 포함하고 있다고 설명했는지, 그리고 왜 ‘감싸다’라는 표현을 사용했는지 이해가 되십니까? 옵셔널은 Enum이기 때문에, 아래와 같은 구문도 사용할 수 있습니다. let age: Int? = 20 switch age { case .none: // `nil`인 경우print(\"나이 정보가 없습니다.\") case .some(let x) where x < 20: print(\"청소년\") case .some(let x) where x < 65: print(\"성인\") default: print(\"어르신\") }8. 프로토콜프로토콜Protocol은 인터페이스입니다. 최소한으로 가져야 할 속성이나 메서드를 정의합니다. 구현은 하지 않습니다. 진짜로 정의만 합니다. /// 전송가능한 인터페이스를 정의합니다. protocol Sendable { var from: String? { get } var to: String { get } func send() } 클래스와 구조체에 프로토콜을 적용Conform시킬 수 있습니다. 프로토콜을 적용하면, 프로토콜에서 정의한 속성와 메서드를 모두 구현해야 합니다. struct Mail: Sendable { var from: String? var to: String func send() { print(\"Send a mail from \\(self.from) to \\(self.to)\") } } struct Feedback: Sendable { var from: String? { return nil// 피드백은 무조건 익명으로 보냅니다. } var to: String func send() { print(\"Send a feedback to \\(self.to)\") } } 프로토콜은 마치 추상클래스처럼 사용될 수 있습니다. func sendAnything(_ sendable: Sendable) { sendable.send() } let mail = Mail(from: \"ac85kr@gmail.com\", to: \"gksxowns93@ohcomon.com\") sendAnything(mail) let feedback = Feedback(from: \"ac85kr@gmail.com\") sendAnything(feedback) sendAnything() 함수는 Sendable 타입을 파라미터로 받습니다. Mail와 Feedback은 엄연히 다른 타입이지만, 모두 Sendable을 따르고 있으므로 sendAnything() 함수에 전달될 수 있습니다. 그리고, Sendable에서는 send() 함수를 정의하고 있기 때문에 호출이 가능합니다. 프로토콜은 또다른 프로토콜을 따를 수 있습니다. protocol Messagable { var message: String? { get } } protocol Sendable: Messagable { ... } Sendable은 Messagable을 기본적으로 따르는 프로토콜입니다. 따라서, Sendable을 적용하려면 var message: String? { get }을 정의해주어야 합니다.Any는 모든 타입에 대응합니다. AnyObject는 모든 객체Object에 대응합니다. let anyNumber: Any = 10 let anyString: Any = \"Hi\" let anyInstance: AnyObject = Dog() Any와 AnyObject는 프로토콜입니다. Swift에서 사용 가능한 모든 타입은 Any를 따르도록 설계되었고, 모든 클래스들에는 AnyObject 프로토콜이 적용되어있습니다.타입 캐스팅 (Type Casting) anyNumber에 10을 넣었다고 해서 anyNumber가 Int는 아닙니다. ‘Any 프로토콜을 따르는 어떤 값’이기 때문입니다. anyNumber + 1// 컴파일 에러!이럴 때에는 as를 이용해서 다운 캐스팅Down Casting을 해야 합니다. Any는 Int보다 더 큰 범위이기 때문에, 작은 범위로 줄인다고 하여 ‘다운 캐스팅’입니다. Any는 Int 뿐만 아니라 String과 같은 전혀 엉뚱한 타입도 포함되어 있기 때문에 무조건 Int로 변환되지 않습니다. 따라서 as?를 사용해서 옵셔널을 취해야 합니다. let number: Int? = anyNumber as? Int옵셔널이기 때문에, 옵셔널 바인딩 문법도 사용할 수 있습니다. 실제로 이렇게 사용하는 경우가 굉장히 많습니다. if let number = anyNumber as? Int { print(number + 1) } 타입 검사 타입 캐스팅까지는 필요 없고, 만약 어떤 값이 특정한 타입인지를 검사할 때에는 is를 사용할 수 있습니다. print(anyNumber isInt) // true print(anyNumber isAny) // true print(anyNumber isString) // false print(anyString isString) // trueSwift 주요 프로토콜 Swift에는 기본적으로 제공하는 기초적인 프로토콜들이 있습니다. 알아두면 개발할 때 굉장히 유용하게 사용할 수 있습니다. CustomStringConvertible 자기 자신을 표현하는 문자열을 정의합니다. print(), String() 또는 “()”에서 사용될 때의 값입니다. CustomStringConvertible의 정의는 아래와 같이 생겼습니다. public protocol CustomStringConvertible { /// A textual representation of `self`. public var description: String { get } } 실제로 적용시… struct Dog: CustomStringConvertible { var name: Stringvar description: String { return\"🐶 \\(self.name)\" } } let dog = Dog(name: \"찡코\") print(dog) // 🐶 찡코ExpressibleBy 우리는 지금까지 10은 Int, “Hi”는 String이라고 ‘당연하게’ 인지하고 있었습니다. 하지만, 엄밀히 하자면 10은 원래 Int(10)으로 선언되어야 하고, “Hi”는 String(“Hi”)로 선언되어야 합니다. Int와 String 모두 생성자를 가지는 구조체이기 때문입니다. 이렇게, 생성자를 사용하지 않고도 생성할 수 있게 만드는 것을 리터럴Literal이라고 합니다. 직역하면 ‘문자 그대로’라는 뜻입니다. 아래 코드는 문자 그대로 10, 문자 그대로 “Hi”, 문자 그대로 배열이고 딕셔너리입니다. let number = 10 let string = \"Hi\" let array = [\"a\", \"b\", \"c\"] let dictionary = [ \"key1\": \"value1\", \"key2\": \"value2\", ] 이 리터럴을 가능하게 해주는 프로토콜이 바로 ExpressibleByXXXLiteral 입니다. Int는 ExpressibleByIntegerLiteral을, String은 ExpressibleByStringLiteral을, Array는 ExpressibleByArrayLiteral을, Dictionary는 ExpressibleByDictionaryLiteral 프로토콜을 따르고 있습니다. 각 프로토콜은 리터럴 값을 받는 생성자를 정의하고 있습니다. struct DollarConverter: ExpressibleByIntegerLiteral { type alias IntegerLiteralType = Int let price = 1_177 var dollars: Int init(integerLiteral value: IntegerLiteralType) { self.dollars = value * self.price } } let converter: DollarConverter = 100 converter.dollars // 117700Tip: typealias는 C의 typedef와 같습니다. typealias MyInt = Int라고 하면, 새로 생성된 MyInt는 Int와 완전히 동일한 타입입니다. 프로토콜에서도 typealias를 정의할 수 있습니다.Tip: 1177은 가독성을 위해 1_177로 쓸 수 있습니다. 12_345는 12345랑 같아요. 1234_5도 12345와 같습니다. 분명히 구조체를 만들었는데, ExpressibleByIntegerLiteral을 적용하니까 = 100과 같은 문법을 사용할 수 있게 되었습니다. let oddEvenFilter: OddEvenFilter = [1, 3, 5, 2, 7, 4] oddEvenFilter.odds // [1, 3, 5, 7] oddEvenFilter.evens // [2, 4] 9. 익스텐션Swift에서는 이미 정의된 타입에 새로운 속성이나 메서드를 추가할 수 있습니다. 익스텐션Extension이라는 기능인데, extension 키워드를 사용해서 정의할 수 있습니다. extension String { var length: Int { return self.characters.count } func reversed() -> String { return self.characters.reversed().map { String($0) }.joined(separator: \"\") } } let str = \"안녕하세요\" str.length // 5 str.reversed() // 요세하녕안 //응용하기: 거꾸로 된 문자열을 반환하는 대신에, 자기 자신을 거꾸로 바꿔버리는 reverse().. 힌트: mutating 키워드와 self 여기까지 Swift 언어의 기본 및 사용방법에 대해 알아보았으며, 다음부터는 코르도바를 활용한 블루투스 프로젝트 생성 예제에 대해 포스트 해보겠습니다. 감사합니다.",
"url": "/ios-swift2"
}
,
"ios-swift": {
"title": "[iOS] Swift 기본 (1)",
"author": "Simplehan",
"category": "",
"content": "1. 변수와 상수변수는(variable)은 값을 수정할 수 있고, 상수(constant)는 그렇지 않습니다. 변수는 var로 선언하고, 상수는 let으로 선언합니다. var name = \"simple\" let birthyear = 1234변수일 경우엔 다음과 같이 변경이 가능하지만 name = \"han\"상수일 경우에는 컴파일 에러가 발생합니다. birthyear=2000//컴파일에러Swift는 javascript와는 다르게 정적 타이핑 언어(java / C)입니다. 변수 또는 상수를 선언시 그 자료형(type)을 명시해주어야 하는 언어를 말합니다. var name: String = \"simple\" let birthyear: Int = 1234 var height: Float = 100.1이런식으로 변수 또는 상수이름 뒤에 콜론(:)을 붙이고 자료형을 명시할 수 있습니다. 이때 사용하는 :String, :Int 등을 가지고 타입 어노테이션(Type Annotation) 이라고 합니다. Swift에서는 타입을 굉장히 엄격히 다루기 때문에 다른 자료형 끼리는 기본연산조차 되지않습니다. 예를 들어 현재 Int로 선언한 birthyear와 Float 타입의 money를 더하려고 할 경우 에러가 발생합니다. birthyear + height // 컴파일 에러!이런 경우엔 정확히 어떤 타입으로 표현할건지 명시후 사용하면 됩니다. Float(birthyear) + height // 1334.1해당 방식을 조금 응용하면, String(birthyear) + \"의 \" + name + \"임\"// 1234의 simple임좀더 쓰기 편하게 바꿀 경우 아래와 같이 작성할 수도 있습니다. \"\\(birthyear)의 \\(name)임\"// 1234의 simple임위의 예제에서는 자료형을 명시하지 않았음에도 에러가 발생하지 않는 것을 알 수있는데, Swift 컴파일러도 큰 따옴표(”)로 감싸진 텍스트는 String 타입인 것을 알고, 숫자는 Int 타입인 것을 인식할 수 있기 때문입니다. 이처럼 직접 명시하지않고도 값을 가지고 정적 타이핑을 할 수 있게 해주는 것을 타입 추론(Type Inference)이라고 합니다.2. 배열과 딕셔너리배열과 딕셔너리는 모두 대활호([]) 를 이용해서 정의할 수 있습니다. var languages = [\"Swift\", \"Objective-C\", \"Python\"] var capitals = [ \"한국\": \"서울\", \"일본\": \"도쿄\", \"중국\": \"베이징\" ]접근하거나 값을 변경시에도 대괄호를 사용합니다. languages[0] // Swift languages[1] = \"Ruby\" capitals[\"한국\"] // 서울 capitals[\"프랑스\"] = \"파리\"마찬가지로 상수로 정의하면 값을 수정할 수 없으므로 주의하셔야합니다. 또한 타입 명시시에는 대괄호안에 어떤 타입을 받을것인지 명시 해야합니다. var languages: [String] = [\"Swift\", \"Objective-C\", \"Python\"] var capitals: [String: String] = [ \"한국\": \"서울\", \"일본\": \"도쿄\", \"중국\": \"베이징\", ]빈 배열이나 빈 딕셔너리를 정의하고싶을경우엔 빈 대괄호를 쓰면 됩니다. var languages: [String] = [] var capitals: [String: String] = [:] var languages = [String]() var capitals = [String: String]()3. 조건문과 반복문조건을 검사할 때에는 if, switch를 씁니다. 아래 코드는 if를 사용한 예시입니다. var age = 19 var student = \"\" if age >= 8 && age < 14 { student = \"초등학생\" } else if age < 17 { student = \"중학생\" } else if age < 20 { student = \"고등학생\" } else { student = \"기타\" } student // 고등학생if문의 조건절에는 값이 정확하게 참 혹은 거짓으로 나오는 Bool 타입을 사용해야 합니다. 위에서 언급한 것과 같이 Swift에서는 타입 검사를굉장히 엄격하게 하기 때문에, 다른 언어에서 사용 가능한 아래와 같은 코드를 사용하지 못합니다. var number = 0 if !number { /* 컴파일 에러!*/ ...} Unary operator ‘!’ cannot be applied to an operand of type ‘Int’ 대신 이렇게 써야합니다. if number == 0 { // ... } 빈 문자열이나 배열 등을 검사할 때에도 명확하게 길이가 0인지를 검사해야 합니다. if name.isEmpty { ... } if languages.isEmpty { ... } 만약 C나 Java와 같은 프로그래밍 언어를 사용해봤다면 switch는 단순히 값이 ‘같은지’만을 검사하는 것으로 알고 있겠지만, Swift의 switch구문은 조금 특별합니다. 패턴 매칭이 가능하기 때문입니다. 아래 코드는 위에서 작성한 if문을 switch문으로 옮겨본 것입니다. switch age { case8..<14: student = \"초등학생\" case14..<17: student = \"중학생\" case17..<20: student = \"고등학생\" default: student = \"기타\" }8..<14와 같이 범위Range 안에 age가 포함되었는지 여부를 검사할 수 있습니다. 반복되는 연산을 할 때에는 for, while을 씁니다. for 구문을 사용해서 배열과 딕셔너리를 차례로 순환할 때에는 아래와 같이 씁니다. for language in languages { print(\"저는 \\(language) 언어를 다룰 수 있습니다.\") } for (country, capital) in capitals { print(\"\\(country)의 수도는 \\(capital)입니다.\") } 단순한 반복문을 만들고 싶다면 범위를 만들어서 반복시킬 수도 있습니다. for i in0..<100 { i } 만약 i를 사용하지 않는데 단순한 반복을 하고 싶다면, i 대신 _를 사용해서 무시할 수도 있습니다. for_in0..<10 { print(\"Hello!\") } -키워드는 어디서나 변수 이름 대신에 사용할 수 있으며, 알아두면 유용하게 사용할 수 있습니다. while은 조건문의 값이 true일 때 계속 반복됩니다. var i = 0 while i < 100 { i += 1 }4. 옵셔널Swift가 가지고 있는 가장 큰 특징 중 하나가 바로 옵셔널Optional입니다. 직역하면 ‘선택적인’ 이라는 뜻이 되는데요. 값이 있을 수도 있고 없을 수도 있는 것을 나타냅니다.예를 들어 문자열의 값이 있으면 “가나다”가 될 것입니다. 그럼, 값이 없다면 ““으로 출력 될것인가? 아닙니다. ““도 엄연히 값이 있는 문자열입니다. 정확히는 ‘값이 없다’가 아니고 ‘빈 값’입니다. 값이 없는 문자열은 바로 nil입니다.또 다른 예를 들어보겠습니다. 정수형의 값이 있으면 123과 같은 값이 있을 것입니다. 값이 없다면 0인가? 마찬가지로 0은 0이라는 숫자 ‘값’입니다. 이 경우에도 값이 없는 정수는 nil입니다. 마찬가지로, 빈 배열이나 빈 딕셔너리라고 해서 ‘값이 없는’것이 아닙니다. 다만 ‘비어 있을’ 뿐입니다.. 배열과 딕셔너리의 경우에도 ‘없는 값’은 nil입니다.이렇게, 값이 없는 경우를 나타낼 때에는 nil을 사용합니다. 그렇다고 해서 모든 변수에 nil을 넣을 수 있는 것은 아닙니다. 예로, 우리가 위에서 정의한 name이라는 변수에 nil을 넣으려 하면 에러가 발생합니다. var name: String = \"simple\" name = nil// 컴파일 에러! Nil cannot be assigned to type ‘String’값이 있을 수도 있고 없을 수도 있는 변수를 정의할 때에는 타입 어노테이션에 ?를 붙여야 합니다. 이렇게 정의한 변수를 바로 옵셔널Optional이라고 합니다. 옵셔널에 초깃값을 지정하지 않으면 기본값은 nil입니다. var email: String? print(email) // nil email = \"ac85kr@gmail.com\"print(email) // Optional(\"ac85kr@gmail.com\")옵셔널로 정의한 변수는 옵셔널이 아닌 변수와는 다릅니다. 예를 들어, 아래와 같은 코드는 사용할 수 없습니다. let optionalEmail: String? = \"ac85kr@gmail.com\" let requiredEmail: String = optionalEmail // 컴파일 에러! Value of optional type ‘String?’ not unwrapped; did you mean to use ‘!’ or ‘?’?requiredEmail 변수는 옵셔널이 아닌 String이기 때문에 항상 값을 가지고 있어야 합니다. 반면에, optionalEmail은 옵셔널로 선언된 변수이기 때문에 실제 코드가 실행되기 전까지는 값이 있는지 없는지 알 수 없습니다. 따라서 Swift 컴파일러는 안전을 위해 requiredEmail에는 옵셔널로 선언된 변수를 대입할 수 없게 만들었습니다. 옵셔널은 개념적으로 이렇게 표현할 수 있습니다. 어떤 값 또는 nil을 가지고 있습니다. ,-- 어떤 값 (String, Int, ...) Optional `-- nil 옵셔널 바인딩 (Optional Binding)그럼 옵셔널의 값을 가져오고 싶은 경우에는 어떻게 해야합니까? 이 때 사용하는 것이 바로 옵셔널 바인딩Optional Binding입니다. 옵셔널 바인딩은 옵셔널의 값이 존재하는지를 검사한 뒤, 존재한다면 그 값을 다른 변수에 대입시켜줍니다. if let 또는 if var를 사용하는데ㅡ 옵셔널의 값을 벗겨서 값이 있다면 if문 안으로 들어가고, 값이 nil이라면 그냥 통과하게 됩니다. 예를 들어, 아래의 코드에서 optionalEmail에 값이 존재한다면 email이라는 변수 안에 실제 값이 저장되고, if문 내에서 그 값을 사용할 수 있습니다. 만약 optionalEmail이 nil이라면 if문이 실행되지 않고 넘어갑니다. if let email = optionalEmail { print(email) // optionalEmail의 값이 존재한다면 해당 값이 출력됩니다. } // optionalEmail의 값이 존재하지 않는다면 if문을 그냥 지나칩니다.하나의 if문에서 콤마(,)로 구분하여 여러 옵셔널을 바인딩할 수 있습니다. 이곳에 사용된 모든 옵셔널의 값이 존재해야 if문 안으로 진입합니다. var optionalName: String? = \"한태준\" var optionalEmail: String? = \"ac85kr@gmail.com\" if let name = optionalName, email = optionalEmail { /* name과 email 값이 존재*/ } //Tip: 코드가 너무 길 경우에는, 이렇게 여러 줄에 걸쳐서 사용하는 것이 바람직합니다. If let name = optionalName, let email = optionalEmail { /* name과 email 값이 존재*/ } 참고로, 두 번째 let 부터는 생략이 가능합니다. 위 코드는 아래 코드와 동일합니다. if let name = optionalName { if let email = optionalEmail { /* name과 email 값이 존재*/ } } /* Tip: 한 번의 if문에서 여러 옵셔널을 바인딩할 수 있게 된 것은 Swift 1.2 버전부터입니다. 이전 버전까지는 바로 위와 같이 여러 번으로 감싸진 옵셔널 바인딩을 사용했습니다. 옵셔널을 바인딩할 때 ,를 사용해서 조건도 함께 지정할 수 있습니다. 이후의 조건절은 옵셔널 바인딩이 일어난 후에 실행됩니다. 즉, 옵셔널이 벗겨진 값을 가지고 조건을 검사하게 됩니다. */ var optionalAge: Int? = 22 if let age = optionalAge, age >= 20 { /* age의 값이 존재하고, 20 이상입니다.*/ } 위 코드는 아래 코드와 동일합니다. if let age = optionalAge { if age >= 20 { /* age의 값이 존재하고, 20 이상입니다.*/ } } 옵셔널 체이닝 (Optional Chaining) : Swift 코드를 간결하게 만들어주는 많은 요소들이 있는데, 옵셔널 체이닝Optional Chaining을 알게되면 다른 프로그래밍 언어가 조금 불편하게 느껴지는 경우가 생깁니다. 옵셔널 체이닝을 이해하는 데에는 설명보다 코드를 보는 편이 훨씬 좋습니다. 예컨대, 옵셔널로 선언된 어떤 배열을 떠올려봅시다. 이 배열이 ‘빈 배열’인지를 검사하려면 어떻게 해야 하겠습니까? nil이 아니면서 빈 배열인지를 확인해보면 될 것입니다. let array: [String]? = [] var isEmptyArray = false if let array = array, array.isEmpty { isEmptyArray = true } else { isEmptyArray = false } isEmptyArray /*옵셔널 체이닝을 사용하면 이 코드를 아래와 같이 더 간결하게 쓸 수 있습니다.*/ let isEmptyArray = array?.isEmpty == true옵셔널 체이닝은 옵셔널의 속성에 접근할 때, 옵셔널 바인딩 과정을 ? 키워드로 줄여주는 역할을 합니다. 다음과 같이 3가지 경우의 수를 생각해봅시다. /*array가 nil인 경우*/ array?.isEmpty ~~~~~~ 여기까지 실행되고 `nil`을 반환합니다. /*array가 빈 배열인 경우*/ array?.isEmpty ~~~~~~~~~~~~~~ 여기까지 실행되고 `true`를 반환합니다. /*array에 요소가 있는 경우*/ array?.isEmpty ~~~~~~~~~~~~~~ 여기까지 실행되고 `false`를 반환합니다. array?.isEmpty의 결과로 나올 수 있는 값은 nil, true, false가 됩니다. isEmpty의 반환값은 Bool인데, 옵셔널 체이닝으로 인해 Bool?을 반환하도록 바뀐 것입니다. 따라서 값이 실제로 true인지를 확인하려면, == true를 해주어야 합니다. 옵셔널 벗기기 : 옵셔널을 사용할 때마다 옵셔널 바인딩을 하는 것이 가장 바람직합니다. 하지만, 개발을 하다보면 분명히 값이 존재할 것임에도 불구하고 옵셔널로 사용해야 하는 경우가 종종 있는데, 이럴 때에는 옵셔널에 값이 있다고 가정하고 값에 바로 접근할 수 있도록 도와주는 키워드인 !를 붙여서 사용하면 됩니다. print(optionalEmail) /* Optional(\"ac85kr@gmail.com\") */ print(optionalEmail!) /* ac85kr@gmail.com*/!를 사용할 때에는 주의할 점이 있는데, 옵셔널의 값이 nil인 경우에는 런타임 에러가 발생한다는 것입니다. Java의 NullPointerException과 비슷하다고 생각하시면 될 듯 합니다. var optionalEmail: String? print(optionalEmail!) // 런타임 에러! fatal error: unexpectedly found nil while unwrapping an Optional value런타임 에러가 발생하면 iOS 앱은 강제로 종료(크래시)됩니다. 그러므로 조심히 사용할 필요가 있습니다. 암묵적으로 벗겨진 옵셔널 (Implicitly Unwrapped Optional) : 만약, 옵셔널을 정의할 때 ? 대신 !를 붙이면 ImplicitlyUnwrappedOptional이라는 옵셔널로 정의됩니다. 직역하면 ‘암묵적으로 벗겨진 옵셔널’입니다. var email: String! = \"ac85kr@gmail.com\" print(email) // ac85kr@gmail.com이렇게 정의된 옵셔널은 nil을 포함할 수 있는 옵셔널이긴 한데, 접근할 때 옵셔널 바인딩이나 옵셔널을 벗기는 과정을 거치지 않고도 바로 값에 접근할 수 있다는 점에서 일반적인 옵셔널과 조금 다릅니다. 옵셔널 벗기기와 마찬가지로, 값이 없는데 접근을 시도하면 런타임 에러가 발생합니다. var email: String! print(email) // 런타임 에러! fatal error: unexpectedly found nil while unwrapping an Optional value가급적이면 일반적인 옵셔널을 사용해서 정의하고, 옵셔널 바인딩 또는 옵셔널 체이닝을 통해 값에 접근하는 것이 더 바람직합니다. Tip: Swift 2 버전에서는 “(email)”과 같이 문자열을 포맷팅하면 ac85kr@gmail.com이 나왔으나, Swift 3 버전부터는 ImplicitlyUnwrappedOptional을 문자열 포맷팅 할 경우 Optional(“ac85kr@gmail.com”)로 포맷팅되니 주의해서 사용해야 합니다. Swift 3 버전부터 ImplicitlyUnwrappedOptional을 일반 Optional과 거의 동일하게 취급했기 때문인데, 자세한 이유는 SE-0054 문서에 잘 나와있습니다. ",
"url": "/ios-swift"
}
,
"ios-xcode": {
"title": "[iOS] Xcode 설치 프로젝트 생성",
"author": "Simplehan",
"category": "",
"content": "1. iOS 개발 환경 세팅 (Xcode 설치 및 기본 설정)Xcode 다운로드2. Xcode 사용 방법 최초 실행 처음 실행시 위와 같은 화면이 출력됩니다. iOS 프로젝트 생성방법 상단 그림의 Create a new Xcode project를 클릭시 하단의 이미지가 출력 됩니다. 화면에서 Single View Application으로 Next 할 시 다음과 같은 화면이 출력됩니다. Product name : 프로젝트 이름입니다. 프로젝트 생성후 따로 설정하지 않으면, 이 값이 앱의 이름으로 사용됩니다. 앱 이름은 추 후 따로 설정 가능하므로 UpperCamelCase로 작성하는 것을 권장합니다. Organization Name : 소스코드 상단에 자동 추가되는 Copyright text를 생성하는데 사용됩니다. Organiztion Identifier : Bundle Identifier를 생성하는되 사용되는 prefix입니다. 위와 같이(com.examle) 도메인을 뒤집어서 사용하는 것이 관례입니다. Bundle Indentifier : 모든 앱이 가지는 고유 식별자입니다. Organization Identifier와 Productname을 합친 문자열을 사용하는 것이 관례입니다. Language : Swift 또는 Objective-C를 선택하여 개발할 수 있습니다. Devices : 애플리케이션이 구동되는 기기입니다. iPhone, iPad, Universal을 선택 가능혀며, 차이는 iPhone으로 설정하여 생성된 앱은 iPad에서도 구동은 가능하지만 해상도와 레이아웃은 iPhone을 따라 가게 됩니다. Use Core Data : Core Data는 디스크에 저장되는 모델링 프레임워크입니다. Include Unit Tests : 유닛 테스트 타겟을 자동으로 생성할지 선택합니다. Include UI Tests : UI 테스트 타겟을 자동으로 생성할지 선택합니다. 설정을 마친후 Finish후 프로젝트를 생성하고 싶은 경로를 선택하여 Create하여 프로젝트를 생성하면 다음과 같은 프로젝트 설정 화면이 출력됩니다. 해당 화면에서 설정했던 값을 수정가능하며, Signing 탭은 초기 생성시 none값이 들어가 있을것인데, 해당 값은 애플 개발자 계정 등록해주시면 됩니다. (일반 계정도 등록은 가능하지만 권한부여에대한 제약이 있습니다.) iOS 프로젝트 그 후 상단을 보시면Run 버튼과 우측의 Device 선택 탭, 상태등을 보여주는 탭이있는데Run 버튼 (재생버튼)을 클릭시 Build Succeeded 후 선택된 Device로 실행됩니다. (Xcode에는 일단 기본적으로 모든 iPhone의 기종 시뮬레이터가 있으며, 원하는 기종을 선택가능합니다, 물론 실제 기기연결시에도 출력됩니다.)실행후 정상 빌드가 완료되면 이미지와 같은 화면이 출력되며, 시뮬레이터에 아무것도 출력되지않는 모습을 볼 수 있습니다.해당 View page 는 기본적으로storyboard에서 쉽게 수정가능합니다 (Android의 Layout - View기능과 동일하며 Storyboard (Layout) 에서 추가 가능합니다) 여기까지 iOS IDE인 Xcode 설치및 기본 프로젝트 생성 방법에 대해 알아봤으며,앞으로 iOS 개발 언어인 Swift의 기본적인 부분이나 사용 방법을 포스팅을 하겠습니다.",
"url": "/ios-xcode"
}
,
"tensorflow-pycharm-anaconda": {
"title": "[Tensorflow] Pycharm 설치 및 Anaconda 연동",
"author": "Simplehan",
"category": "",
"content": "1. Pycharm 다운로드 및 설치 (community 버전)Pycharm 다운로드2. Pycharm - Anaconda 가상환경과 연동 프로젝트 생성 create new project 선택 프로젝트 저장위치 지정 후 ▼ Project interpreter : Python 3.6 부분 선택 Existing interpreter 부분 경로 우측 … 버튼 선택 Add Python Interpreter 창 내에 Conda Enviroment 탭 선택 우측 Conda executable 우측 폴더 아이콘 선택 후 [설치한 Anaconda 경로₩envs₩생성한 가상환경₩python.exe ] 선택 후 OK 버튼 선택 Existing interpreter 에 새로 지정한 경로가 입력되어 있는지 확인 후 create 3. 가상환경과 연결 되어 있는지 확인가상환경을 생성할 때 tensorflow 패키지를 같이 생성하였었다. tensorflow 가 잘 생성 되었고, 제대로 동작 하는지 확인을 해보겠다. Pyhon file 생성 다음 코드 입력 import osimport tensorflow as tf os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' hello = tf.constant(\"hello\") sess = tf.Session()print('version : ', tf.__version__)print(sess.run(hello)) os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' 이 부분은 Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX AVX2 경고 문구를 없애기 위한 부분이다. 위 코드를 실행해 보면 결과는 다음과 같다. tensorflow 버전과 hello 라는 문구가 찍혀 있는것을 볼 수 있다. 이것으로 Tensorflow를 공부하기 전에 필요한 환경과 Tool 을 셋팅 하였다. 앞으로 포스팅에는 이 환경을 이용하여 Tensorflow를 이용한 Deeplearning 에 대하여 다루도록 하겠다.",
"url": "/tensorflow_pycharm_anaconda"
}
,
"tensorflow-install-anaconda": {
"title": "[Tensorflow] Anaconda(아나콘다) 설치하기",
"author": "Simplehan",
"category": "",
"content": "아나콘다 파이썬을 가상환경으로 사용할 수 있도록 지원하는 프로젝트중 하나이다. 파이썬 및 주로 사용되는 1400 여개의 패키지와 데이터 과학 패키지들이 포함되어 있어 파이썬 설치 후 여러 패키지들을 설치해야 하는 번거로움을 해결해 준다. 윈도우즈에서 쉽게 가상환경을 만들고 버전 관리를 할 수 있도록 도와준다. 설치 시간이 좀 오래 걸린다는 단점이 있다.윈도우용 아나콘다 설치 아나콘다 다운로드 페이지에서 자신에게 맞는 운영체제의 파일을 다운로드한다. (Windows용 텐서플로 바이너리 패키지는 현재 파이썬 3.5, 3.6, 3.7버전을 지원한다.) 파이썬 설치프로그램 실행 Next > 를 선택한다. License Agreement I Agree 선택 Select Installation Type just me 선택 (All user 를 선택할시 C 드라이브 프로그램에 설치가 된다. 로컬PC 에서 테스트 용을 사용할 시에는 just me 를선택할 것을 권장한다.) Choose Install Location 로컬 PC 사용자 폴더에 설치가 되어진다 (사용자 계정이 한글일 경우 다른 경로를 입력 하라는 경고창이 뜨게 된다. 한글이 포함되지 않은 경로를 재설정 하여 설치를 계속 진행한다.) Advanced Options Add Anaconda to my PATH enviroment variable 아나콘다를 PATH 환경 변수에 추가할지 여부를 선택하는 부분 환경 변수에 추가 하면 윈도우 CMD 창에서 conda 명령어를 사용할 수 있으면 선택하지 않을 경우 Anaconda prompt 또는 Anaconda navigator 를 실행하여야 conda 명령어를 사용할 수 있다. 다른 Python 은 설치 하지 않고 Anaconda 를 주력으로 사용할 경우에만 체크할 것을 권유한다. Regster Anaconda as my default Python 3.7 아나콘다를 기본 파이썬으로 등록 할지 여부를 선택한다. 선택 시 개발 도구나 에디터에서 아나콘다를 파이썬으로 인식하게 된다. 설치완료 - 1 (VSCode 설치여부) 이미 설치 되어 있거나 VSCode를 에디터로 사용하지 않을 경우는 Skip을 선택한다. 설치 완료 시작 메뉴 확인 가상환경 설정 꼭 python 3.7 을 사용 하지 않고 다른 버전의 python 을 사용하기 위한 가상 환경을 생성 할 수 있다. 나중에 Pycharm 과 연동하여 다양한 환경에서의 개발을 진행 할 수 있다. Anaconda prompt 실행 Anaconda Navigator를 이용하여 GUI 환경에서 설정이 가능하지만 이번에는 prompt 명령어를 통해서 가상환경을 생성해 본다. 설치한 아나콘다가 맞는지 버전을 확인해 본다 $ conda --version 가상환경 생성 가상환경을 생성한다. 이 때 필요한 패키지들을 나열해서 생성시 한번에 설치할 수 있다. $ conda create --name [MY_ENV_NAME] [package1 package2 ....] 이번 시간엔 Tensorflow 를 사용하기 위해서 파이썬 3.6 버전과 tensorflow 와 keras 를 명시하여 설치해 보겠다. $ conda create --name TEST_ENV python=3.6 tensorflow keras 생성 진행 화면 (base) C:\\Users\\이한얼>conda create --name TEST_ENV python=3.6 tensorflow kerasSolving environment: done ==> WARNING: A newer version of conda exists. <== current version: 4.5.4 latest version: 4.7.11 Please update conda by running $ conda update -n base conda ## Package Plan ## environment location: D:\\Anaconda3\\envs\\TEST_ENV added / updated specs: - keras - python=3.6 - tensorflow The following packages will be downloaded: package | build ---------------------------|----------------- _tflow_select-2.3.0 | mkl 3 KB numpy-1.16.4 | py36h19fb1c0_0 49 KB numpy-base-1.16.4 | py36hc3f5095_0 4.1 MB wrapt-1.11.2 | py36he774522_0 48 KB pyyaml-5.1.2 | py36he774522_0 163 KB intel-openmp-2019.4 | 245 1.7 MB ca-certificates-2019.5.15 | 1 166 KB tensorflow-base-1.14.0 |mkl_py36ha978198_0 54.5 MB scipy-1.3.1 | py36h29ff71c_0 14.4 MB python-3.6.9 | h5500b2f_0 20.4 MB termcolor-1.1.0 | py36_1 8 KB wheel-0.33.4 | py36_0 57 KB astor-0.8.0 | py36_0 45 KB absl-py-0.7.1 | py36_0 158 KB mkl-service-2.0.2 | py36he774522_0 63 KB h5py-2.9.0 | py36h5e291fa_0 969 KB six-1.12.0 | py36_0 22 KB sqlite-3.29.0 | he774522_0 962 KB mkl_fft-1.0.14 | py36h14836fe_0 137 KB mkl_random-1.0.2 | py36h343c172_0 318 KB icc_rt-2019.0.0 | h0cc432a_1 9.4 MB tensorflow-1.14.0 |mkl_py36hb88db5b_0 4 KB keras-2.2.4 | 0 5 KB mkl-2019.4 | 245 157.5 MB protobuf-3.8.0 | py36h33f27b4_0 582 KB zlib-1.2.11 | h62dcd97_3 128 KB libmklml-2019.0.5 | 0 21.4 MB gast-0.2.2 | py36_0 138 KB setuptools-41.0.1 | py36_0 663 KB pyreadline-2.1 | py36_1 141 KB libprotobuf-3.8.0 | h7bd577a_0 2.2 MB werkzeug-0.15.5 | py_0 256 KB markdown-3.1.1 | py36_0 132 KB pip-19.2.2 | py36_0 1.9 MB certifi-2019.6.16 | py36_1 156 KB keras-applications-1.0.8 | py_0 33 KB tensorflow-estimator-1.14.0| py_0 291 KB tensorboard-1.14.0 | py36he3c9ec2_0 3.3 MB openssl-1.1.1c | he774522_1 5.7 MB keras-preprocessing-1.1.0 | py_1 36 KB hdf5-1.10.4 | h7ebc959_0 19.2 MB keras-base-2.2.4 | py36_0 458 KB grpcio-1.16.1 | py36h351948d_1 931 KB ------------------------------------------------------------ Total: 322.9 MB The following NEW packages will be INSTALLED: _tflow_select: 2.3.0-mkl absl-py: 0.7.1-py36_0 astor: 0.8.0-py36_0 blas: 1.0-mkl ca-certificates: 2019.5.15-1 certifi: 2019.6.16-py36_1 gast: 0.2.2-py36_0 grpcio: 1.16.1-py36h351948d_1 h5py: 2.9.0-py36h5e291fa_0 hdf5: 1.10.4-h7ebc959_0 icc_rt: 2019.0.0-h0cc432a_1 intel-openmp: 2019.4-245 keras: 2.2.4-0 keras-applications: 1.0.8-py_0 keras-base: 2.2.4-py36_0 keras-preprocessing: 1.1.0-py_1 libmklml: 2019.0.5-0 libprotobuf: 3.8.0-h7bd577a_0 markdown: 3.1.1-py36_0 mkl: 2019.4-245 mkl-service: 2.0.2-py36he774522_0 mkl_fft: 1.0.14-py36h14836fe_0 mkl_random: 1.0.2-py36h343c172_0 numpy: 1.16.4-py36h19fb1c0_0 numpy-base: 1.16.4-py36hc3f5095_0 openssl: 1.1.1c-he774522_1 pip: 19.2.2-py36_0 protobuf: 3.8.0-py36h33f27b4_0 pyreadline: 2.1-py36_1 python: 3.6.9-h5500b2f_0 pyyaml: 5.1.2-py36he774522_0 scipy: 1.3.1-py36h29ff71c_0 setuptools: 41.0.1-py36_0 six: 1.12.0-py36_0 sqlite: 3.29.0-he774522_0 tensorboard: 1.14.0-py36he3c9ec2_0 tensorflow: 1.14.0-mkl_py36hb88db5b_0 tensorflow-base: 1.14.0-mkl_py36ha978198_0 tensorflow-estimator: 1.14.0-py_0 termcolor: 1.1.0-py36_1 vc: 14.1-h0510ff6_4 vs2015_runtime: 14.15.26706-h3a45250_4 werkzeug: 0.15.5-py_0 wheel: 0.33.4-py36_0 wincertstore: 0.2-py36h7fe50ca_0 wrapt: 1.11.2-py36he774522_0 yaml: 0.1.7-hc54c509_2 zlib: 1.2.11-h62dcd97_3 Proceed ([y]/n)? y Downloading and Extracting Packages_tflow_select-2.3.0 | 3 KB | ###################################### | 100%numpy-1.16.4 | 49 KB | ###################################### | 100%numpy-base-1.16.4 | 4.1 MB | ###################################### | 100%wrapt-1.11.2 | 48 KB | ###################################### | 100%pyyaml-5.1.2 | 163 KB | ###################################### | 100%intel-openmp-2019.4 | 1.7 MB | ###################################### | 100%ca-certificates-2019 | 166 KB | ###################################### | 100%tensorflow-base-1.14 | 54.5 MB | ###################################### | 100%scipy-1.3.1 | 14.4 MB | ###################################### | 100%python-3.6.9 | 20.4 MB | ###################################### | 100%termcolor-1.1.0 | 8 KB | ###################################### | 100%wheel-0.33.4 | 57 KB | ###################################### | 100%astor-0.8.0 | 45 KB | ###################################### | 100%absl-py-0.7.1 | 158 KB | ###################################### | 100%mkl-service-2.0.2 | 63 KB | ###################################### | 100%h5py-2.9.0 | 969 KB | ###################################### | 100%six-1.12.0 | 22 KB | ###################################### | 100%sqlite-3.29.0 | 962 KB | ###################################### | 100%mkl_fft-1.0.14 | 137 KB | ###################################### | 100%mkl_random-1.0.2 | 318 KB | ###################################### | 100%icc_rt-2019.0.0 | 9.4 MB | ###################################### | 100%tensorflow-1.14.0 | 4 KB | ###################################### | 100%keras-2.2.4 | 5 KB | ###################################### | 100%mkl-2019.4 | 157.5 MB | ##################################### | 100%protobuf-3.8.0 | 582 KB | ###################################### | 100%zlib-1.2.11 | 128 KB | ###################################### | 100%libmklml-2019.0.5 | 21.4 MB | ###################################### | 100%gast-0.2.2 | 138 KB | ###################################### | 100%setuptools-41.0.1 | 663 KB | ###################################### | 100%pyreadline-2.1 | 141 KB | ###################################### | 100%libprotobuf-3.8.0 | 2.2 MB | ###################################### | 100%werkzeug-0.15.5 | 256 KB | ###################################### | 100%markdown-3.1.1 | 132 KB | ###################################### | 100%pip-19.2.2 | 1.9 MB | ###################################### | 100%certifi-2019.6.16 | 156 KB | ###################################### | 100%keras-applications-1 | 33 KB | ###################################### | 100%tensorflow-estimator | 291 KB | ###################################### | 100%tensorboard-1.14.0 | 3.3 MB | ###################################### | 100%openssl-1.1.1c | 5.7 MB | ###################################### | 100%keras-preprocessing- | 36 KB | ###################################### | 100%hdf5-1.10.4 | 19.2 MB | ###################################### | 100%keras-base-2.2.4 | 458 KB | ###################################### | 100%grpcio-1.16.1 | 931 KB | ###################################### | 100%Preparing transaction: doneVerifying transaction: doneExecuting transaction: done## To activate this environment, use## $ conda activate TEST_ENV## To deactivate an active environment, use## $ conda deactivate 가상 환경이 생성 되었다. 이제 생성한 가상 환경으로 접속을 진행해 본다. ※ 가상환경을 생성한 뒤에 pip을 통해 필요한 패키지들을 설치하는것도 가능하다. 가상환경 접속 $ conda activate [MY_ENVS_NAME] 생성한 가상환경 이름으로 접속 $ conda activate TEST_ENV 설치한 python 버전 확인 (TEST_ENV) C:\\Users\\이한얼>python --version conda 기본 명령어들 # 가상환경 종료 $ conda deactivate #가상환경 목록 화인 $ conda env list #가상환경 삭제 $ conda remove --name [MY_ENVS_NAME] --all 가상환경 생성시 설치한 tensorflow 확인 tensorflow 패키지가 정상적으로 설치 되었는지 버전 확인 python 명령어로 python shell 진입 $ python tensorflow import 후 버전확인 $ import tensorflow as tf$ tf.__version__ Anaconda를 이용한 가상환경 생성 및 기본적으로 쓰이는 명령어에 대해서 알아 보았다. 다음 포스팅에는 Anaconda를 통해 생성한 가상환경을 Python IDE 중 하나인 Pycharm과 연동하여 Python을 이용하는 방법을 알아보겠다.",
"url": "/tensorflow_install_anaconda"
}
,
"google-analytics": {
"title": "minimal-mistakes에 Google Analytics 연동하기",
"author": "Simplehan",
"category": "",
"content": "블로그를 운영하는데 통계가 필요하다. 다행히 minimal-mistake 테마에서 Google Analytics 연동을 지원한다. 그래서 Google Analytics와 블로그를 연동해보기로 했다. (Google Analytics 외에 다른 통계 소스 연동도 지원하고 커스텀을 하는 방법도 지원한다. 커스텀을 이용하는 방법은 아직 해본 적이 없다.)일단 Google Analytics 사이트로 이동하자.처음 접속을 하면 “무료로 이용하기” 와 같은 글이 떠있는 것을 보니 무료인 것 같다. (해당 첫 접속 페이지 이미지를 깜빡하고 캡처하지 못해서 남기질 못했다. 가입 후에는 나오질 않아서 추후 이미지를 얻게 되면 업로드 예정.)계정 가입 시 첫번째로 위와 같이 계정 이름을 입력해줘야 한다. 이 계정 이름은 자신이 원하는 대로 지정해주자. 그 후 자신에게 필요한 옵션을 아래 체크박스에서 체크한 뒤 다음을 눌러주자.다음으로 자신의 측정 대상을 선택해주자. 나는 웹 기반의 이 블로그를 측정할 것이기 때문에 웹을 선택했다.마지막으로 자신의 측정 대상의 정보를 입력해주자. 나는 웹을 선택했기 때문에 위와 같이 나오는데 다른 대상을 선택했으면 다른 세부정보 입력 화면이 나오니 잘 읽고 입력해주자.가입이 완료되면 위와 같이 추적 id (tracking id)를 확인할 수가 있는데 이를 복사해두자.그 후 minimal-mistakes 기반 블로그의 _config.yml 파일을 열면 analytics 항목이 있는데 아래와 같이 작성해주자.analytics: provider: \"google\" google: tracking_id: \"위에서 복사한 추적 id\" anonymize_ip: false # true, false (default)그 후 블로그에 설정을 반영한 뒤 analytics 사이트에서 통계가 제대로 집계되고 있는 지 확인해보자.제대로 집계가 된다면 블로그 접속 시 analytics 홈에 위와 같이 현재 활성 사용자가 카운트된다. 집계가 되는 것이 확인되면 좌측 메뉴에서 여러가지 통계를 볼 수 있으니 잘 활용해보자.FAQQ1 : 추적 ID를 어디서 확인하는 지 모르겠어요.추적 ID는 기본적으로 최초에 계정을 생성하면 생성 완료 후 보여주게 되는데, 이를 확인하지 못했거나 웹사이트 에러, 브라우자 에러 등등 모종의 이유로 확인을 하지 못했을 시를 대비해서 추후에 확인할 수 있는 방법이 있다. 아래 이미지를 보고 추적 ID를 확인하자. 좌측 하단의 관리 클릭. 속성 탭 확인 추적 정보 탭의 추적 코드 클릭. 상단의 추적 ID 확인.",
"url": "/google-analytics"
}
,
"ssh-how-to-setup-ssh": {
"title": "[Ubuntu 18.04] SSH 접속 설정 하기",
"author": "Simplehan",
"category": "",
"content": "SSH 란?SSH (Secure Shell Protocol) 이란 네트워크 프로토콜중 하나로, 컴퓨터와 컴퓨터가 Public Network를 통해 서로 통신(연결)을 할때 보안적으로 안전하게 사용하기 위한 프로토콜Ubuntu 18.04 에 SSH 적용하기1. openssh-server 설치 유무 확인$ dpkg -l | grep openssh※ 우분투를 설치하시게 되면 기본적으로 openssh-client 는 설치가 되어있을 것이다.2.openssh-server 설치$ apt-get install openssh-server3. openssh-server 설치 확인$ dpkg -l | grep openssh3.Putty 로 ssh 접속 확인하기- server ip address 확인$ ifconfig- Putty 접속 확인우분투 설치시 설정한 계정과 비밀번호를 입력 하면 로그인이 되는 것을 확인할 수 있다.",
"url": "/ssh-how_to_setup_ssh"
}
,
"write-post": {
"title": "VSCode, Typora를 이용하여 jekyll 포스트 작성하기 (with minimal-mistakes)",
"author": "Simplehan",
"category": "",
"content": "jekyll 포스트를 작성하는 방법에 대해 작성하려 한다. 나는 공개된 테마 중 minimal-mistakes 테마를 이용하고 있으므로 해당 테마 기반으로 글을 작성한다. 그리고 jekyll 사용 편의성을 높히기 위해 VSCode와 Typora를 같이 이용하기로 하였다.jekyll, minimal-mistakes 등에 관한 설명은 지난 포스트를 참조하자.minimal-mistakes 테마 로컬에서 구동하기포스트 작성 전에 VSCode와 Typora를 설치해보자.VSCode 설치VSCode는 Visual Studio Code의 약칭으로 마이크로소프트가 제작한 소스 코드 편집기이다. 이를 이용해서 jekyll 프로젝트의 편집을 할 계획이다.VSCode를 받기 위해서 VSCode 사이트에 접속한다. 사이트에서 Installer를 다운로드 받자.VSCode 설치가 끝난 뒤 실행을 해보면 아래와 같은 화면이 나온다.VSCode의 폴더 열기를 이용하여 자신의 소스 코드 폴더를 열면 아래처럼 화면이 변한다. 이제부터 소스 코드 편집을 진행할 수 있다.Typora 설치VSCode를 이용하여 파일을 빠르게 작성하는 것은 좋으나, jekyll 포스트는 기본적으로 파일 형식이 md이다. md파일은 마크다운 포맷으로 구성되기 때문에 작성과 출력에 차이가 있다. 이 포맷을 작성하는 도중에도 확인할 수 있다면 더욱 편하게 포스트 작성이 가능하기 때문에 마크다운 에디터인 Typora를 이용하기로 했다.Typora를 설치하기 위해 Typora 사이트에 접속한다. Download 탭에서 자신의 OS에 맞는 버전을 선택하여 다운받는다. (나는 Windows로 진행하였다.)Typora를 다운받은 뒤 실행해보면 아래와 같은 화면이 나온다.이제부터 Typora를 이용하여 마크다운 문서를 작성할 수 있다. Typora의 다양한 기능을 이용하여 마크다운 문서를 작성해보자.Typora 상단 메뉴 중 본문, 서식 등을 이용하여 문서를 작성할 수 있는데 문서를 작성하면 실시간으로 마크다운 문서가 어떻게 해석되서 나타나는 지 볼 수 있다. 아래 이미지처럼 말이다.물론 소스 코드 상태로도 볼 수 있는데 Typora 아래쪽의 </> 아이콘을 클릭하면 소스 코드 모드로 변환된다. 이 상태에서 위의 문서를 보게 되면 아래와 같이 변한다.현재 작성한 이 포스트도 Typora에서 작성한 포스트이다. 앞으로 포스트들은 이를 이용해서 작성될 예정이다.이제 준비가 끝났으니 VSCode를 이용하여 첫 포스트를 작성해보자.포스트 작성하기포스트를 작성하기 위해선 일단 _posts 폴더가 필요하다. jekyll은 _posts 폴더에 특정한 이름의 마크다운 파일을 자동으로 포스트로 인식한다. 이 폴더가 있다면 상관없지만 없다면 다음과 같이 폴더를 생성해주자.폴더를 생성했다면 다음으로 포스트를 작성해볼 차례인데, 주의점이 있다. 파일의 이름 형식을 꼭 맞춰줘야 하는데, 파일 형식은 다음과 같다. YEAR-MONTH-DAY-title.MARKUP YEAR : 4자리의 숫자MONTH, DAY : 2자리의 숫자title : 포스트 제목 (파일 이름 정도로 인식하면 된다.)MARKUP : 마크다운 포맷이 인식되는 확장자 이를 토대로 만들어진 올바른 포스트 파일명은 아래와 같다. 2011-12-31-new-years-eve-is-awesome.md2012-09-12-how-to-write-a-blog.md이제 첫 포스트를 작성해보자. 파일 이름은 2019-08-01-first-post.md 로 지정했다.이제 간단히 글을 작성해보자. jekyll 문서들은 첫 문단에 머리글(Front Matter)을 작성할 수 있는데 문서의 각종 설정, 메타 데이터라고 인식하면 된다. 첫 포스트의 머리글은 아래와 같이 작성할 수 있는데 이미지 아래에서 설명하겠다. 이름 기능 title 포스트 제목 categories 포스트 카테고리 정의 tags 포스트 태그 정의 date 작성일 이 외에도 정말 다양한 설정값이 존재하나 추후 포스트에서 설명하겠다.머리글 작성이 끝나면 그 아래에 작성하는 내용이 포스트의 본문이 된다. 테스트용으로 간단하게 작성해보자.작성이 다 끝났으면 자신의 로컬서버, 혹은 github pages 서버에서 포스트가 잘 작성되었는지 확인해보자. (github pages를 이용하는 방법은 추후 포스팅 예정이다.)첫 포스트가 잘 올라간 모습을 볼 수 있다. (나는 minimal-mistakes 테마의 다른 설정들도 건드린 상태이기 때문에 순정 상태와 모습이 다르다.)",
"url": "/write-post"
}
,
"run-on-local": {
"title": "minimal-mistakes 테마 로컬에서 구동하기",
"author": "Simplehan",
"category": "",
"content": "블로그를 시작하였다. 처음엔 티스토리를 이용하여 블로그를 개설하려 했으나 github에서 무료로 제공하는 github pages 호스팅 서비스가 있길래 이를 이용해보기로 했다. github pages는 jekyll로 동작하기 때문에 jekyll의 공개된 테마 중 인기가 많은 minimal-mistakes 테마를 이용하였다.이 jekyll (혹은 jekyll 기반의 테마)을 이용하여 자신만의 페이지를 구성 후 github의 repository에 소스를 push하면 해당 소스가 배포되어 정적 웹 사이트가 호스팅되기 시작한다.그러나 push를 한다고 해서 반영 및 배포가 바로바로 되지는 않기 때문에 지금 보고 있는 페이지가 수정이 완료가 된건지, 아직 배포가 안된건지 알기가 힘들다. 그리고 잘못 수정된 파일이 올라갈수도 있다. 이러한 문제점들을 방지하기 위해 local 환경에서 사이트를 띄워볼 수 있는데 여기에서 그 방법을 설명하려 한다.Git 설치minimal-mistakes 테마는 github에 올라가 있기 때문에 이 소스를 내려받기 위해선 git이 필요하다. (git이 없어도 zip 파일로 소스를 내려받을 순 있지만 추후 자신의 소스를 github에 올리기 위해서도 필요하기 때문에 git을 설치하는 것이 좋다.)git을 설치하기 위해서 git 다운로드 페이지에 접속하자. 다운로드 페이지에서 자신의 OS에 맞게 다운로드를 받아서 설치를 진행하자.특별히 옵션을 선택할 게 없으면 Next만 쭉 눌러서 설치해주자.git 설치가 완료되면 cmd, powershell, git bash 등의 터미널에서 git을 이용할 수 있게 된다.위의 이미지는 git bash에서 git 버전을 확인해본 모습이다.minimal-mistakes 소스코드 내려받기minimal-mistakes github에서 소스를 clone 한다. cmd에서 진행했다. (minimal-mistakes github에서 자신의 github로 fork 받는 방법도 있다. 본인이 편한 방법으로 소스를 내려받자.)git clone https://github.com/mmistakes/minimal-mistakes.gitclone받은 소스에서 불필요한 파일들을 제거해야된다. 그 파일들은 다음과 같다: .editorconfig .gitattributes .github /docs /test CHANGELOG.md minimal-mistakes-jekyll.gemspec README.md screenshot-layouts.png screenshot.png그러면 아래와 같은 구조가 완성된다. (구조가 100% 맞지 않을 수 있다.)minimal-mistakes├── _data # 테마 커스터마이징 데이터| ├── navigation.yml # 네비게이션(메뉴) 데이터| └── ui-text.yml # UI의 텍스트 (언어별)├── _includes| ├── analytics-providers # snippets for analytics (Google and custom)| ├── comments-providers # snippets for comments| ├── footer # custom snippets to add to site footer| ├── head # custom snippets to add to site head| ├── feature_row # feature row helper| ├── gallery # image gallery helper| ├── group-by-array # group by array helper for archives| ├── nav_list # navigation list helper| ├── toc # table of contents helper| └── ...├── _layouts| ├── archive-taxonomy.html # tag/category archive for Jekyll Archives plugin| ├── archive.html # 아카이브| ├── categories.html # 카테고리로 묶은 아카이브| ├── category.html # 특정 카테고리로 묶은 아카이브| ├── collection.html # 특정 콜렉션으로 묶은 아카이브| ├── compress.html # compresses HTML in pure Liquid| ├── default.html # 기본| ├── home.html # 홈| ├── posts.html # 년도별로 묶은 아카이브| ├── search.html # 검색| ├── single.html # single document (post/page/etc)| ├── tag.html # 특정 태그로 묶은 아카이브| ├── tags.html # 태그로 묶은 아카이브| └── splash.html # 스플래시 페이지├── _sass # SCSS├── assets| ├── css| | └── main.scss # main stylesheet, loads SCSS partials from _sass| ├── images # image assets for posts/pages/collections/etc.| ├── js| | ├── plugins # jQuery plugins| | ├── vendor # vendor scripts| | ├── _main.js # plugin settings and other scripts to load after jQuery| | └── main.min.js # optimized and concatenated script file loaded before </body>├── _config.yml # 설정파일├── Gemfile # gem file dependencies├── index.html # 인덱스└── package.json # NPM build scriptsminimal-mistakes 테마를 받는 방법은 clone, fork 외에도 존재한다. gem기반으로 내려받는 방법도 있고, config 파일에서 remote를 지정하여 소스를 내려받지 않고 network 상으로 이용하는 방법도 존재한다. 해당 방법들은 minimal-mistakes 가이드 글의 Quick-Start Guide를 참조하자.Ruby 설치minimal-mistakes 테마는 위에서도 언급했다시피 jekyll 기반으로 만들어져 있다. 그러므로 이 소스를 로컬 환경에서 구동하기 위해선 jekyll이 필요한데, jekyll은 Gem이라는 Ruby 기반의 패키지 매니저를 통해서 받을 수 있다. 이를 위해 먼저 Ruby를 설치해야 한다. RubyInstaller에 접속해서 Installer를 다운받도록 하자.설치 중에 MSYS2 development toolchain도 설치할건지 체크박스가 나오는데 같이 설치해주자.설치중에 위와 같이 선택지가 나오는데 1번 옵션 (MSYS2 base installation)을 선택해서 설치했다.Devkit이 포함된 Ruby를 설치하면 Gem도 사용이 가능해진다. 이제 gem을 이용해서 jekyll과 gem 관리를 위한 bundler를 설치해보자. 진행은 Windows cmd에서 진행했다.gem install jekyll bundler이제 소스코드 폴더로 들어가서 의존성들을 다운로드 받아준다.bundle install의존성 다운로드가 끝나면 구동을 해보자.bundle exec jekyll serve\t# --port xxxx 옵션을 통하여 포트 지정 가능포트를 지정했다면 해당 포트로, 포트를 지정하지 않았다면 4000 포트로 접속 시 블로그가 뜨면 성공.에러 caseInvalid CP949 character “\\xE2”로컬에서 구동 시 아래와 같은 에러가 출력될 수 있다.Conversion error: Jekyll::Converters::Scss encountered an error while converting 'assets/css/main.scss': Invalid CP949 character \"\\xE2\" on line 54jekyll 3.8.5 | Error: Invalid CP949 character \"\\xE2\" on line 54jekyll 홈페이지에서 windows 환경에서 구동 시 인코딩에 문제가 있다고 소개하고 있는데 다음과 같이 설명하고 있다. 만약 UTF-8 인코딩을 사용한다면, 문서 안에 BOM 헤더를 사용하지 않아야 합니다. 그렇지 않으면 Jekyll 에 아주, 아주 안 좋은 일이 벌어집니다. 이는 특히, 윈도우즈에서 Jekyll 을 사용하는 것에 연관된 문제입니다. 그리고, 사이트 생성 단계에서 “Liquid Exception: Incompatible character encoding” 에러가 발생하는 경우엔, 콘솔창의 코드 페이지를 UTF-8 로 바꿔야 할 수도 있습니다.이를 위한 해결법 역시 적혀있는데 terminal에서 다음과 같이 입력을 해주면 된다.chcp 65001그러면 terminal에서 Active code page: 65001 이라는 텍스트가 출력이 되는데 이제 아래의 실행코드를 실행하면 서버가 구동되는 모습을 확인할 수 있다.bundle exec jekyll serve\t// --port xxxx 옵션을 통하여 포트 지정 가능http://jekyllrb-ko.github.io/docs/windows/#%EC%9D%B8%EC%BD%94%EB%94%A9incompatible encoding regexp match로컬에서 구동 시 아래와 같은 에러가 출력될 수 있다....jekyll 3.8.5 | Error: incompatible encoding regexp match (CP949 regexp with UTF-8 string)이는 jekyll 프로젝트 경로 상에 한글명의 폴더가 들어가 있는 경우이다. 한글을 바꿔주거나 다른 경로로 이동하여 다시 실행해보자.There was an error parsing `Gemfile`[!] There was an error parsing `Gemfile`: There are no gemspecs at [folder]. Bundler cannot continue. # from [folder]/Gemfile:2 # ------------------------------------------- # source \"https://rubygems.org\" > gemspec # source \"https://rubygems.org\" # -------------------------------------------bundle install 시 Gemfile이 없거나 파일이 제대로 작성되어 있지 않을 때 위와 같은 에러가 출력될 수 있다. Gemfile이 이상이 없는지 확인해보자. 참고용으로 현재 이 블로그의 Gemfile 내용을 아래에 작성해 둘테니 잘 안된다면 복사해서 붙여넣어보자.source \"https://rubygems.org\"gem \"github-pages\", group: :jekyll_pluginsgem \"tzinfo-data\"gem \"wdm\", \"~> 0.1.0\" if Gem.win_platform?# If you have any plugins, put them here!group :jekyll_plugins do gem \"jekyll-paginate\" gem \"jekyll-sitemap\" gem \"jekyll-gist\" gem \"jekyll-feed\" gem \"jemoji\" gem \"jekyll-algolia\" gem \"jekyll-include-cache\"end",
"url": "/run-on-local"
}
,
"ssl-nginx-tomcat": {
"title": "Nginx + Tomcat8 연동",
"author": "Simplehan",
"category": "",
"content": "지난 시간에는 nginx 에 let’s encrypt 를 이용하여 무료 SSL 인증서를 적용해 보았다. 이번 시간에는 SSL 인증서로 https 접속이 가능해진 nginx 에 톰캣을 연결해 보도록 할 것이다.nginx 설치 방법은 Letsencrypt + Nginx SSL인증서(HTTPS) 적용 포스팅에서 nginx 설치 부분을 참고환경 Ubuntu 18.04 apt-get 을 통해 설치한 NginxNginx 와 Tomcat 연동(톰캣이 설치&실행 되어 있다는 가정 하에 진행)Nginx 의 설정 파일을 수정 ( 여기서는 /etc/nginx/site-available/default 파일을 수정)$ vim /etc/nginx/site-available/defaultserver 블록 상단에 톰캣 서버에 대한 경로를 지정해 준 뒤 server 블록 내부에 location 설정 부분 수정 - 80포트와 443 포트에 대해 톰캣으로 연결되도록 설정을 수정한다....# 톰캣 위치를 지정upstream tomcat {\tip_hash;\tserver 127.0.0.1:8080;}# 80 포트에 대한 설정 server { listen 80; listen [::]:80; root /var/www/html; # Add index.php to the list if you are using PHP index index.html index.htm index.nginx-debian.html; server_name _; charset utf-8; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. # try_files $uri $uri/ =404; // css 가 로드되지 않을수 있으므로 주석처리 # proxy 설정 추가 proxy_pass http://tomcat; //tomcat 서버 지정 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; \t\t} } ... 중략 # 443 포트에 대한 설정 server { listen 443; listen [::]:443; ssl on; server_name _; ssl_certificate /etc/letsencrypt/live/8e7dcf95.ngrok.io/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/8e7dcf95.ngrok.io/privkey.pem; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. # try_files $uri $uri/ =404; // css 가 로드되지 않을수 있으므로 주석처리 # proxy 설정 추가 proxy_pass http://tomcat; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; }}default 파일 내 location 설정 안에proxy_pass http://tomcat;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header Host $http_host;이 구문을 추가하면 된다.파일을 저장한 뒤 nginx service 재시작$ service nginx restart다시 서버 ip로 접속시에 기존 nginx 페이지가 아닌 톰캣 서버 index 페이지가 나타나는 것을 확인 할 수 있을 것이다. (http, https 둘다 접속시도 해보자)http 접속https 접속※ 상단 바 주의 요함부분을 클릭해보면 인증서가 적용되어 있을 것이다.서버 재시작후 웹 브라우저에서 접속을 실행하면 500번대 에러가 날 수 있다. 이럴땐 톰캣 서버가 활성화 되어 있는지 확인해 본 뒤 다시 접속을 해보자",
"url": "/ssl-nginx-tomcat"
}
,
"ssl-letsencrypt": {
"title": "Letsencrypt + Nginx SSL인증서(HTTPS) 적용",
"author": "Simplehan",
"category": "",
"content": "환경 Ubuntu 18.04 apt-get 을 통해 설치한 Nginx 도메인 ( 이번 과정에서는 도메인이 없는 경우를 대비 ngrok 을 이용한 임시 도메인 사용 )Let’s Encrypt 웹 사이트에 HTTPS (SSL / TLS)를 사용하기 위해 필요한 디지털 인증서를 무료로 제공 만료 기간이 90일, 만료 기간전에 수동으로 인증서를 갱신 또는 crontab, certbot auto 를 활용한 자동 갱신 자세한 내용은 Let’s Encrypt 공식 사이트 에서 확인Nginx 설치$ sudo apt-get install nginx80 포트를 사용하는 또다른 서비스가 존재한다면 서비스를 중지하거나 삭제 한 후 nginx 설치설치완료 후 서버 포트 번호로 웹 브라우저에서 접속 시 nginx 화면이 나오는지 확인한다.Nginx 화면이 나오지 않을 시에는$ sudo service nginx restart명령어로 nginx 를 재시작 해본 뒤 다시 웹 브라우저에서 접속시도 nginx index 화면이 제대로 나온다면 이제 Let’s Encrypt 를 이용하여 SSL 인증서를 받아보자certbot 설치apt-get 에 certbot 저장소 추가$ sudo add-apt-repository ppa:certbot/certbot패키지 목록을 업데이트 한 후에 certbot 을 설치$ sudo apt-get update$ sudo apt-get install python-certbot-nginxSSL 인증을 하기에 앞서 구매한 도메인이 없다면, certbot 에서 해당 서버에 접근을 할 수가 없어 인증이 되지 않는다. 잠시 테스트용으로 ngrok 을 사용하여 임시 도메인을 사용해 테스트를 해보자Ngrok 설치 및 실행ngrok 홈페이지에 들어가 간단한 회원가입 절차를 진행한 후에 운영체제에 맞는 ngrok 을 다운 받은 뒤 매뉴얼에 따라 ngrok을 실행해 보자 (이번 포스트 환경은 ubuntu 64bit 이므로 linux 를 다운로드한다.)③ 번 까지 진행을 했다면 ngrok을 압춘을 푼 폴더에서$ sudo ./ngrok http http://localhost를 실행 해보자 그러면 이러한 화면이 나올 것이다.Fowarding 부분에 있는 http 주소를 복사( CTRL + C 는 ngrok을 중지시키는 단축키이므로 GUI 가 있는 운영체제라면 마우스 오른쪽 클릭을 활용하거나 직접 주소를 타이핑 하자!!!) 하여 웹 브라우저에서 들어가보면 nginx index 창이 뜨는걸 확인할 수 있다.이제 ngrok을 실행 시켜 놓은 상태에서 새로운 터미널 창을 연뒤 certbot 을 이용해 인증서를 발급해 보도록 하자Let’s Encrypt 인증서 발급설치한 certbot 을 이용하여 인증서를 발급 받을 것이다. 여기에선 인증서만 필요하므로 certonly 형식으로 인증서만 서버에 받아 오도록 진행 해보자$ sudo certbot certonly --nginx -d [도메인 주소]저 도메인 주소에 ngrok 으로 생성한 도메인을 넣어 주도록 하자$ sudo certbot certonly --nginx -d 8e7dcf95.ngrok.io도메인 주소 부분에 http 또는 ‘ / ‘ 가 포함되지 않도록 주의 하자명령어를 실행하면 서버 인증이 실행 되고Congratulation! 이라는 단어와 함께 인증이 완료되었다고 나올 것이다.인증 완료 문구에서 보면/etc/letsencrypt/live/8e7dcf95.ngrok.io/fullchain.pem/etc/letsencrypt/live/8e7dcf95.ngrok.io/privkey.pem같은 경로가 보일 텐데 이 경로가 인증서가 저장되어진 경로이다. 해당 경로로 들어가 인증 된 파일을 확인해 보자총 4가지의 파일이 존재 할 것이다. 이중 fullchain.pem 과 privkey.pem 두가지만 nginx SSL 설정에 사용할 것이다.Nginx SSL 설정Nginx 의 설정 파일을 수정 ( 여기서는 /etc/nginx/site-available/default 파일을 수정)$ sudo vim /etc/nginx/site-available/default파일 내부를 확인해 보면 80 port 설정에 대한 server 블록이 존재할 것이다. 그 server 블록 아래에 443(SSL) port 에 대한 server 블록을 추가해 주자# 80 포트의 대한 설정은 수정 하지 않는다. server {\t ... listen 80; listen [::]:80; root /var/www/html; index index.html index.htm index.nginx-debian.html; server_name _; charset utf-8; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } ...}# 443 포트를 설정하기 위한 server 블록 추가server { listen 443; listen [::]:443; ssl on; // ssl 을 활성화 server_name _; ssl_certificate /etc/letsencrypt/live/8e7dcf95.ngrok.io/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/8e7dcf95.ngrok.io/privkey.pem; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; }}ssl on 을 통해 SSL 을 활성화 시키고ssl_certificate 에 발급받은 인증서 중 fullchain.pem 이 위치한 경로를 지정해 주고ssl_certificate_key 에 발급받은 인증서 중 privkey.pem 이 위치한 경로를 지정해 준다.파일을 저장 하고 나온 뒤 nginx 서버를 재시작 하자!$ sudo service nginx restart서버 재시작이 정상적으로 완료 되면 ngrok 에서 발급 받은 주소가 아닌 서버 포트 번호를 통해 HTTPS 보안 접속을 시도해 보자연결이 비공개로 설정 되어 있다는 화면이 나올 것이다. 당황하지 말고 하단 고급탭을 선택 후 “192.168.107.128(안전하지 않음)(으)로 이동” 을 클릭해 보자nginx index 화면이 정상적으로 나올 것이다.여기서 인증서가 적용이 잘 되었는지 확인을 하기 위해 웹 브라우저 주소검색줄 부분 왼쪽을 보면 주의요함 이라는 글씨가 보일 것이다. 이부분을 클릭한뒤 나오는 메뉴 중에서 인증서 탭을 선택해 보자인증서 정보가 나오게 될 텐데 여기서 발급 대상은 ngrok을 통해 생성한 도메인 주소가, 발급자는 Let’s Encrypt 가 유효기간은 90일이 설정되어 보일 것이다.다음 게시글에서는 SSL 설정이 완료된 Nginx 에 Tomcat 을 연동하는 방법을 설명해 보겠다.바로가기: Nginx + Tomcat8 연동",
"url": "/ssl-letsencrypt"
}
};
</script>
<script src="assets/js/lunr.js"></script>
<script src="assets/js/search.js"></script>
</section>
</article>
</div>
</main>
<!-- /post -->
<!-- The #contentFor helper here will send everything inside it up to the matching #block helper found in default.hbs -->
<script>
$(function() {
var $postContent = $(".post-full-content");
$postContent.fitVids();
});
</script>
<!-- Previous/next page links - displayed on every page -->
<!-- The footer at the very bottom of the screen -->
<footer class="site-footer outer">
<div class="site-footer-content inner">
<section class="copyright"><a href="https://simplehanlab.github.io/">SimpleHan Lab</a> © 2021</section>
<section class="poweredby">Proudly published with <a href="https://jekyllrb.com/">Jekyll</a> &
<a href="https://pages.github.com/" target="_blank" rel="noopener">GitHub Pages</a> using
<a href="https://github.com/jekyllt/jasper2" target="_blank" rel="noopener">Jasper2</a></section>
<nav class="site-footer-nav">
<a href="/">Latest Posts</a>
<a href="https://ghost.org" target="_blank" rel="noopener">Ghost</a>
</nav>
</div>
</footer>
</div>
<!-- The big email subscribe modal content -->
<div id="subscribe" class="subscribe-overlay">
<a class="subscribe-overlay-close" href="#"></a>
<div class="subscribe-overlay-content">
<h1 class="subscribe-overlay-title">Search SimpleHan Lab</h1>
<p class="subscribe-overlay-description">
lunr.js를 이용한 posts 검색 </p>
<span id="searchform" method="post" action="/subscribe/" class="">
<input class="confirm" type="hidden" name="confirm" />
<input class="location" type="hidden" name="location" />
<input class="referrer" type="hidden" name="referrer" />
<div class="form-group">
<input class="subscribe-email" onkeyup="myFunc()"
id="searchtext" type="text" name="searchtext"
placeholder="Search..." />
</div>
<script type="text/javascript">
function myFunc() {
if(event.keyCode == 13) {
var url = encodeURIComponent($("#searchtext").val());
location.href = "/search.html?query=" + url;
}
}
</script>
</span>
</div>
</div>
<!-- highlight.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.10.0/components/prism-abap.min.js"></script>
<script>$(document).ready(function() {
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
});</script>
<!-- jQuery + Fitvids, which makes all video embeds responsive -->
<script
src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous">
</script>
<script type="text/javascript" src="/assets/js/jquery.fitvids.js"></script>
<script type="text/javascript" src="https://demo.ghost.io/assets/js/jquery.fitvids.js?v=724281a32e"></script>
<!-- Paginator increased to "infinit" in _config.yml -->
<!-- if paginator.posts -->
<!-- <script>
var maxPages = parseInt('');
</script>
<script src="/assets/js/infinitescroll.js"></script> -->
<!-- /endif -->
<!-- Add Google Analytics -->
<!-- Google Analytics Tracking code -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-xxxxxxxx-x', 'auto');
ga('send', 'pageview');
</script>
<!-- The #block helper will pull in data from the #contentFor other template files. In this case, there's some JavaScript which we only want to use in post.hbs, but it needs to be included down here, after jQuery has already loaded. -->
<script>
$(function() {
var $postContent = $(".post-full-content");
$postContent.fitVids();
});
</script>
<!-- Ghost outputs important scripts and data with this tag - it should always be the very last thing before the closing body tag -->
<!-- ghost_foot -->
</body>
</html>