관련글

2024.09.24 - [🔨 개발/🏢 C# + Revit API] - Windows 운영체제 환경에서의 소프트웨어 배포: .exe와 .msi 파일의 차이

 

C# + Revit API 카테고리의 첫 글이 배포라는 점이 이상하긴 하지만, 회사에서 개발하던 애드인이 드디어 세상의 빛을 볼 때가 되었다. 비주얼 스튜디오에서 처음으로 Release를 선택해 빌드를 하니 기분이 이상하다. 이제 애드인 사용자들이 쉽게 애드인을 설치할 수 있도록 .msi 설치파일을 만들어 볼까 한다. WiX Toolset이라는 툴을 사용하였다.

 

WiX Toolset 설치 및 Visual Studio 프로젝트 생성

https://wixtoolset.org/

 

WiX Toolset

The most powerful set of tools available to create your Windows installation experience.

wixtoolset.org

상당히 올드해 보이는 홈페이지다. Download 메뉴에 들어가면 설치 방법이 나오는데, 그냥 cmd 창에서 커맨드를 입력해서 설치하고, 잘 설치되었는지 버전을 확인하자.

dotnet tool install --gloabl wix
wix --version

버전을 확인했더니 5.0.1+2f00cbe6 이라는 괴랄한 버전이 나온다. 아마 뒤에 달린건 커밋해시가 아닐까 감히 추측해본다. 그리고나서 비주얼스튜디오로 돌아가서 WiX 프로젝트를 만들면 된다고 하는데... 안뜨네? (이 방법으로도 Visual Studio에서 WiX 프로젝트를 생성할 수 있는 방법을 나중에 찾아봐야겠다.)

https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2022Extension

적당히 구글링해서 Visual Studio Marketplace에 있는 WiX v3 - Visual Studio 2022 Extension 이란걸 설치했다. 다시 새로운 프로젝트를 생성해보니 Setup Project for WiX v3 라는 템플릿이 뜬다ㅎㅎㅎ. 추후에 설명하겠지만 위의 Extension은 Visual Studio에서 WiX 프로젝트를 생성하기 위한 템플릿 파일만 제공하는 듯 하고, 진짜 알맹이는 나중에 다시 설치하는 과정이 나온다.

 

Product.wxs 파일 수정 및 빌드

프로젝트에는 Product.wxs라는 기본 설치 스크립트 파일이 덩그러니 존재한다. XML 기반의 문법을 갖고 있고 뭔가 주저리주저리 쓰여 있다. 대략적으로 다음과 같은 구조로 파일이 구성되어 있다.

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="*" Name="SAMOO_evacuationDistanceAnalysis" Language="1033" Version="1.0.0.1" Manufacturer="회사명" UpgradeCode="버전별GUID">

		<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />

		<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="CommonAppDataFolder">
				<Directory Id="Autodesk" Name="Autodesk">
					<Directory Id="Revit" Name="Revit">
						<Directory Id="Addins2023" Name="Addins">
							<Directory Id="Year2023" Name="2023">
								<Component Id="컴포넌트ID" Guid="컴포넌트GUID">
									<File Id="애드인 DLL ID" Source="빌드 완료된 .dll 파일이 존재하는 경로" KeyPath="yes" />
									<File Id="애드인 매니페스트 ID" Source="매니페스트 .addin 파일이 존재하는 경로"/>
								</Component>
							</Directory>
						</Directory>
					</Directory>
				</Directory>
			</Directory>
		</Directory>

		<Feature Id="ProductFeature" Title="제목" Level="1">
			<ComponentRef Id="컴포넌트ID를 한번 더" />
		</Feature>
	</Product>
</Wix>

 

몇 가지 설명을 추가하자면, Product 태그 내의 UpgradeCode와 Component 태그 내에 Guid가 있는데, Visual Studio의 도구 메뉴에서 GUID 생성을 통해 값을 집어넣으면 된다. 이 때, 두 GUID 값을 별개의 값으로 해주자. UpgradeCode는 프로그램 버전 간 업그레이드를 식별하기 위한 고유값이고, Component의 GUID는 설치된 파일을 고유하게 식별하기 위한 코드라고 한다.

Directory 태그를 통해 애드인 .dll 파일과 .addin 파일이 저장될 위치를 지정할 수 있는데, 태그를 여러 번 nested 해서 사용하기 싫다고 "CommonAppDataFolder\Autodesk\Revit\Addins\2023\" 이렇게 붙여 쓰면 빌드에 실패한다. 귀찮아도 한 스텝씩 내려가주자.

KeyPath는 WiX에서 컴포넌트의 핵심 파일 또는 리소스를 지정하는 데 사용된다. 설치된 구성 요소(컴포넌트)가 올바르게 설치되었는지, 수정, 복구할 때 중요한 기준이 되는 파일이나 리소스를 가리킨다. KeyPath는 .dll 파일 하나에만 설정해주자.

이런저런 설정을 마치고 Visual Studio에서 프로젝트를 빌드하면, 오류가 또 뜬다... WiX Toolset을 깔라는 내용이 또 뜨는데(위에서 설치한 Extension은 Visual Studio 템플릿만 설치해주나보다)

https://github.com/wixtoolset/wix3/releases/tag/wix3112rtm

 

Release WiX Toolset v3.11.2 · wixtoolset/wix3

WiX v3.11.2 is the latest recommended maintenance release of WiX v3.11; it contains mitigations in WiX v3.11 for a vulnerability affecting Microsoft.Deployment.Compression.Cab.dll and Microsoft.Dep...

github.com

나는 여기에서 .exe파일을 통해 설치했다. 그랬더니 빌드 잘 된다...

이제 프로젝트/bin/Release/ 에 생성된 .msi 파일을 더블클릭해서 설치를 테스트하면 끝.

728x90

문제상황

개발을 진행하던 중, feat-a 라는 브랜치에서 코드 작업을 하다가 여러 commit이 진행되었다. 그러다 문제가 발생해서 2개의 commit만큼 working tree를 되돌리기 위해 git reset --hard HEAD^ 명령어를 두 번 수행했다. 그럼 두 번 되돌리던 중 앞서 지나간 commit은 어떻게 되는 것이며, 어떻게 되돌릴 수 있을까?

 

해결방법

git reset --hard HEAD^ 명령을 여러 번 수행하면, 현재 브랜치의 HEAD를 과거 커밋으로 이동시키고, 그 시점 이후의 모든 변경 사항을 working tree와 staging area에서 제거한다. 즉, 최근의 커밋 이력이 없어진 것처럼 보이게 된다. 하지만, 앞서 존재했던 커밋 이력은 영구적으로 삭제된 것은 아니다. Git은 이를 dangling commit(떠도는 커밋)이라고 부르며, Git의 내부 데이터 구조에 여전히 남아있다. 이 커밋들은 Git이 자동으로 저장하는 reflog를 통해 추적 가능하다. 커밋 이력을 복구하는 방법을 알알보자.

git reflog 명령어를 사용하면, 브랜치가 이동한 모든 기록을 볼 수 있다. 이전 커밋으로 reset하기 전의 상태를 찾고, 해당 커밋 해시를 복사한다. 그리고 git reset --hard <커밋 해시> 명령어를 사용하여 해당 커밋으로 돌아갈 수 있다.

나의 경우에는, 개발 중 문제가 생긴 코드가 꼬여서 하나하나 해결하기 어려워 이전 커밋으로 되돌린 상태에서 기존의 작업물이 아까워서 나중에 언젠가 도움이 될까 싶어 브랜치로 남겨 두려는 욕심이 있었기에, 새로운 브랜치를 따서 해당 커밋을 HEAD로 잡아두어 코드를 보관하고자 하였다. 따라서 해당 작업을 명령어로 남겨둔다.

git reflog // 여기서 원하는 커밋 해시 찾기 ex.abcd12
git checkout -b temp-feat abcde123 // temp-feat라는 로컬 저장소에 abcd12라는 이력 보관
728x90

JWT를 이용해서 로그인을 구현하다 보면, 토큰을 어디에 저장할 것인지 결정해야 하는 순간이 온다. 로컬스토리지에 저장하기, 세션스토리지에 저장하기, 쿠키에 저장하기 등등 여러 방법들을 마주할 것이다. 나는 주로 로컬스토리지에 저장하는 것으로 구현하곤 하는데, 로컬스토리지라는 것 자체에 궁금증이 생겨 찾아보고 정리해본다.

 

LocalStorage란?

LocalStorage는 브라우저에서 제공하는 웹 저장소로, 웹 애플리케이션의 데이터를 클라이언트 측에 영구적으로 저장할 수 있는 기능이다. LocalStorage는 세션이 종료되거나 브라우저가 닫혀도 데이터를 유지하며, 웹 애플리케이션에서 서버와의 통신 없이도 데이터를 관리할 수 있게 해 준다. HTML5 표준에서 추가된 기능으로, 모든 최신 브라우저가 이 기능을 지원한다.

 

LocalStorage의 메모리 영역

LocalStorage는 각 도메인(Origin)별로 독립된 메모리 공간을 사용하며, 도메인별로 할당된 메모리의 양은 브라우저마다 상이하다. 일반적으로 브라우저에서 할당된 용량은 약 5MB 정도이며, 이 용량 한도 내에서 문자열 형태의 데이터를 저장할 수 있다. Chrome, Firefox, Safari, Edge, Opera 모두 약 5MB 정도의 제한을 두고 있다. LocalStorage는 비휘발성 메모리를 사용하기 때문에, 페이지를 새로고침하거나 브라우저를 닫았다가 다시 열더라도 저장된 데이터를 유지한다.

 

자바스크립트에서 LocalStorage 사용 방법

LocalStorage는 딕셔너리와 같은 키-값 쌍(key-value pair) 형식으로 데이터를 저장한다. 모든 값은 반드시 문자열이어야 하며, 숫자, 객체 등의 다른 데이터 타입은 문자열로 변환한 후에 저장해야 한다.

저장: localStorage.setItem()

localStorage.setItem('key', 'value');
localStorage.setItem('userId', this.userId); // 예시

조회: localStorage.getItem()

let value = localStorage.getItem('key');
console.log(value); // 'value' 출력

삭제: localStorage.removeItem(), localStorage.clear()

localStorage.removeItem('key'); // 'key'에 해당하는 값 삭제
localStorage.clear(); // 모든 데이터 삭제
// 삭제되어 없는 값을 조회하게 되면 null을 반환한다

저장된 데이터 개수 확인: localStorage.length

let length = localStorage.length;
console.log(length); // 저장된 데이터의 개수 출력

저장된 데이터의 키 조회: localStorage.key()

let key = localStorage.key(0);
console.log(key); // 첫 번째로 지정된 키 값 출력

 

LocalStorage가 삭제되는 경우

내가 LocalStorage에 대해 찾아본 이유로, 저장하고 있는 값이 혹시 없어지는 경우가 존재하는지 궁금했다. 기본적으로 LocalStorage는 데이터를 영구적으로 저장하는 공간이다. 따라서, 수동으로 삭제하지 않는 한 데이터가 자동으로 제거되지는 않는다. 브라우저를 닫아도 데이터는 유지되며, 사용자가 직접 localStorage.clear()나 localStorage.removeItem()을 호출하지 않는 이상 계속 보존된다. 다만 몇 가지 경우에는 데이터가 제거될 수 있는데 그러한 경우는 다음과 같다.

  1. 사용자가 브라우저 캐시를 삭제하는 경우
  2. 브라우저나 운영체제가 제공하는 디스크 정리 기능을 사용하는 경우
  3. 특정 브라우저(특히 Safari)는 디스크 공간이 부족할 때 LocalStorage 데이터를 자동으로 정리할 수 있다

 

LocalStorage를 사용하는 적절한 시점

LocalStorage는 데이터를 장기간 유지해야 하는 경우에 적합하다. 주로 다음과 같은 상황에서 유용하게 사용할 수 있다.

  • 사용자 설정 (ex. 다크 모드 여부, 언어 설정 등)을 저장할 때
  • 비로그인 상태에서 사용자의 상태를 유지해야 할 때
  • 캐시 용도로 데이터를 저장해 서버 요청을 줄이고 성능을 개선할 때
  • 쇼핑 카트나 폼 데이터를 저장해 사용자 경험을 향상시킬 때

 

주의사항

LocalStorage를 사용할 때 몇 가지 주의할 점들이 있다. 기본적으로 LocalStorage는 동기적으로 작동하므로, 대량의 데이터를 읽고 쓰는 작업은 성능에 영향을 줄 수 있다. 그리고 모든 데이터는 문자열 형태로 저장되므로, JSON 객체는 JSON.stringify()와 JSON.parse()를 통해 문자열로 변환한 뒤 사용해야 한다. 마지막으로 LocalStorage는 보안 취약점을 가질 수 있다. 민감한 정보를 LocalStorage에 저장하는 것은 바람직하지 않으며 보안이 필요한 데이터는 서버에서 관리하는 것이 좋다.

 

LocalStorage와 다른 웹 저장소의 차이

LocalStorage 외에도 브라우저에는 SessionStorage와 쿠키(Cookie)가 있다. 이들 간의 차이점을 간략하게 정리하면 다음과 같다.

  • LocalStorage: 도메인 별로 저장되며, 브라우저가 닫혀도 데이터가 유지된다. 대개 5MB까지 저장 가능
  • SessionStorage: LocalStorage와 비슷하지만 세션이 끝날 때(즉, 탭이나 창을 닫을 때) 데이터가 제거
  • 쿠키(Cookie): 서버와 클라이언트 간의 데이터 전달을 위해 주로 사용되며, LocalStorage보다 용량이 매우 제한적(4KB)이다. 쿠키는 만료 시간을 설정할 수 있다.
728x90

YAML(YAML Ain't Markup Language) - 사람이 읽기 쉬운 데이터 형식

이름부터 심상치 않은 이녀석... 웹 프론트 개발을 할 때는 자주 만나지는 않았지만, 이미지 생성 AI 모델인 stable diffusion 구동을 위해 이런저런 작업을 하던 도중 YAML 파일의 내용을 바탕으로 아나콘다 가상환경을 생성할 일이 있었다.(requirements.txt 와 비슷한 역할로) 그렇게 접하게 된 YAML 파일. 꼭 가상환경의 구성 외에도 이런저런 config 용도로 많이 사용되는 듯 한데, 이 파일은 어떤 형식인지 궁금해서 찾아보고 정리하게 되었다.

YAML은 사람이 읽기 쉽고 간결한 데이터 직렬화 형식이다. 다른 데이터 형식인 XML이나 JSON과 같은 용도로 사용되지만, 가독성이 높고 작성이 간편한 점에서 차별화된다. YAML은 주로 애플리케이션 설정 파일, 데이터 파일, 구성 정보 등을 저장하는 데 자주 사용된다.

 

1. YAML의 기본 개념

YAML은 데이터를 계층 구조로 표현하며, 들여쓰기를 사용하여 데이터 간의 관계를 나타낸다. 예를 들어, JSON에서는 중괄호({})를 사용하여 데이터를 구조화하지만, YAML에서는 들여쓰기만으로 같은 구조를 나타낼 수 있다. YAML은 프로그래머뿐만 아니라 비전문가도 쉽게 이해할 수 있도록 설계되었다. 구문이 간단하고 사람이 읽기 쉽게 구성되어 있어 설정 파일이나 데이터를 작성할 때 유용하다.

# 서버 설정 정보
server:
  name: my-server
  port: 8080
  enabled: true

# 데이터베이스 설정 정보
database:
  host: localhost
  username: admin
  password: secret
  tables:
    - users
    - products
    - orders

이 예시는 서버와 데이터베이스 설정을 담고 있다. 각 항목은 들여쓰기를 통해 계층 구조를 표현하며, 리스트는 - 기호를 사용하여 정의된다.

 

2. YAML의 주요 특징

  1. 간결한 구문: YAML은 데이터 간의 구조를 들여쓰기로 표현하기 때문에 중괄호나 대괄호가 필요없다. 이는 XML이나 JSON에 비해 훨씬 간결한 표현을 가능하게 한다.
  2. 높은 가독성: YAML은 사람이 읽고 쓰기 쉽게 설계되었다. 데이터가 중첩되거나 복잡해도 들여쓰기를 통해 직관적으로 구조를 파악할 수 있다.
  3. 유연한 데이터 표현: YAML은 다양한 데이터 타입을 지원한다. 문자열, 숫자, 리스트, 딕셔너리 등 프로그래밍에서 자주 사용하는 구조를 YAML에서도 간단하게 표현할 수 있다.
  4. 주석 지원: YAML 파일에서 주석을 사용할 수 있어, 코드 설명이나 주석을 통해 문서화 작업을 쉽게 할 수 있다. 주석은 # 기호로 시작하며, 주석 뒤의 내용은 무시된다.

 

3. YAML 파일의 장점

  1. 쉬운 유지보수: YAML은 설정 파일을 읽고 수정해야 하는 사용자에게 매우 편리한 형식입니다. 코드의 구조를 쉽게 이해할 수 있으며, 주석을 추가해 설명을 더할 수 있어 유지보수가 용이합니다.
  2. 데이터의 직관적인 표현: YAML은 데이터를 시각적으로 직관적으로 표현할 수 있다. 특히, 복잡한 계층구조를 표현할 때도 들여쓰기를 사용해 쉽게 구조를 파악할 수 있다.
  3. 다양한 프로그래밍 언어와의 호환성: YAML은 파이썬, 자바, C#, PHP 등 다양한 프로그래밍 언어에서 지원된다. 대부분의 언어에는 YAML 파서를 쉽게 사용할 수 있는 라이브러리가 존재한다. 파이썬에서는 PyYAML 같은 라이브러리를 통해 YAML 파일을 쉽게 파싱하고 데이터를 처리할 수 있다.

 

4. YAML 사용 예시

  1. 애플리케이션 설정 파일: YAML은 많은 소프트웨어에서 설정 파일로 사용된다. 내가 처음 경험한 YAML 파일도 그러하다. 예를 들어, Python의 Django, Java의 Spring Boot 같은 프레임워크는 설정 파일로 YAML을 사용할 수 있다. 환경 설정이나 데이터베이스 연결 정보 등을 YAML로 저장하여 애플리케이션을 유연하게 구성할 수 있다.
  2. CI/CD 파이프라인: 지속적 통합 및 배포(CI/CD) 도구인 Jenkins, GitLab CI, CircleCI 등에서 YAML을 사용해 빌드 및 배포 파이프라인을 정의할 수 있다. 예를 들어, GitLab CI에서 .gitlab-ci.yml 파일을 사용하여 프로젝트의 빌드 및 테스트 과정을 정의할 수 있다.

 

5. YAML vs JSON

사실 YAML과 JSON은 매우 비슷한 역할을 한다. 두 형식 모두 데이터를 직렬화하기 위한 것이며, 계층 구조를 지원한다. 그러나 YAML은 JSON보다 가독성에 더 중점을 두고 있다. JSON에서는 {, }, : 같은 구분 기호를 많이 사용하지만, YAML은 들여쓰기를 사용하여 이러한 기호를 생략할 수 있다. YAML을 한 번 이해하면 복잡한 데이터 구조도 간단하게 표현할 수 있으니, 잘 숙지해서 사용하면 좋을 듯 하다.

728x90

'🔨 개발 > 📚 개발지식모음집' 카테고리의 다른 글

파이썬 람다 함수 사용법 정리  (2) 2024.11.04
20240903 requirements.txt 문제  (0) 2024.09.03
20240808  (0) 2024.08.08
20240807  (0) 2024.08.07
20240806  (0) 2024.08.06

웹 개발을 하다보니, 가끔 접속부터 아무 창이 뜨지 않는(내가 만든 페이지가 보여지지 않는) 상황이 발생한다. F12를 눌러 개발자 모드로 들어가서 오류 문구를 확인해보면, CORS 에러를 종종 만나곤 한다. 그럴 때는 옆자리의 백엔드 개발 담당자에게 CORS 뜬다고 "이거 왜 이래요?" 하면 해결해주기도 한다. 이참에 CORS가 뭐고, 왜 오류가 나는 것인지 찾아보도록 하겠다.

CORS(Cross-Origin Resource Sharing)는 웹 브라우저가 보안상의 이유로 동일 출처 정책(SOP, Same-Origin Policy)을 우회할 수 있도록 허용하는 메커니즘이다. 웹 페이지에서 외부 도메인(출처)의 자원에 접근할 때, CORS를 통해 서버가 명시적으로 해당 도메인에서의 요청을 허용해야만 브라우저가 그 요청을 수행할 수 있다. 일단은 쉽게 이해하기 위해서, "브라우저에서 현재 보고 있는 화면과 무관한 서버의 파일들을 아무거나 막 열람할 수 없게 해야 하지 않을까?"라는 의문을 가져볼 수 있겠다. 그럼 일단 동일 출처 정책(Same-Origin Policy)가 무엇인지 보자.

 

동일 출처 정책(Same-Origin Policy)란?

SOP(Same-Origin Policy)는 웹 보안 모델 중 하나로, 한 출처(origin)의 웹 페이지가 다른 출처의 리소스에 임의로 접근하는 것을 방지하는 브라우저 보안 기능이다. 출처는 Scheme(프로토콜), Host(도메인), Port의 조합(우리가 흔히 아는 주소)으로 정의된다. 동일 출처 정책은 악의적인 웹사이트가 사용자의 중요한 정보를 무단으로 도용하거나 조작하는 것을 방지하기 위해 도입되었다. 예를 들어,

  • https://luceeverde.tistory.com과 https://luceeverde.tistory.com은 동일 출처
  • http://luceeverde.tistory.com과 https://luceeverde.tistory.com은 다른 출처(프로토콜 차이)
  • https://luceeverde.tistory.com과 https://tistory.com은 다른 출처(서브도메인 차이)
  • https://luceeverde.tistory.com:3000과 https://luceeverde.tistory.com:4000은 다른 출처(포트 차이)

라고 이해할 수 있겠다. 그렇다면 위에서 CORS는 SOP(동일 출처 정책)을 우회할 수 있도록 허용하는 메커니즘이라고 했으니, 왜 우회를 해야하는 상황이 생기는 걸까?

 

CORS의 필요성

SOP(동일 출처 정책)는 보안 측면에서 매우 중요하지만, 실제 애플리케이션에서는 여러 출처에 걸쳐 데이터를 공유해야 하는 경우가 많다. 예를 들어, 프론트엔드 애플리케이션과 API 서버가 다른 도메인에서 실행되는 경우, 웹 페이지는 API 서버에 요청을 보내 자원을 가져와야 한다. 이때, 동일 출처 정책에 의해 차단될 수 있는데, CORS는 이를 우회하는 방법을 제공한다.

 

CORS의 작동 방식

CORS는 서버가 클라이언트로부터 요청을 받을 때, 해당 요청을 허용할지 결정하는 방식으로 동작한다. 서버는 브라우저가 요청한 출처(origin)에 대해 응답 헤더를 통해 요청을 허용할지 명시적으로 지정해야 한다.

  • 프리플라이트 요청(Preflight Request): 클라이언트가 서버로 특정 조건에 해당하는 요청을 보내기 전에 OPTIONS 메서드를 사용하여 사전 요청(프리플라이트 요청)을 한다. 이를 통해 서버가 해당 요청을 허용하는지 확인한다. 프리플라이트 요청 예시와 응답 예시를 한번 보자.
  • 실제 요청: 프리플라이트 요청이 성공하면, 클라이언트는 실제 요청을 서버로 전송할 수 있다.
/*
프리플라이트 요청 예시
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: http://mydomain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

프리플라이트 응답 예시
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://mydomain.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type


프리플라이트 요청 성공 이후


실제 요청 예시
POST /api/data HTTP/1.1
Host: api.example.com
Origin: http://mydomain.com
Content-Type: application/json

실제 응답 예시
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://mydomain.com
*/

그런데 이러한 CORS 관련 문제는 프론트 개발을 하는 내가 뭔가 작업을 진행해서 문제를 고치는 영역이 아닌 걸 보면, 서버에서 어떠한 처리를 통해 문제를 해결했다는 것인데, (실제로 프리플라이트 요청을 내가 만들어서 서버로 쏘도록 한 것은 아닌데 CORS 오류 문제가 해결되었다.)

 

CORS 응답 헤더

CORS 사용 예시 (서버 측 설정)

CORS 관련 문제와 해결 방법

마무리

 

 

 

728x90

어떤 서비스를 만들던, 로그인은 빠질 수 없는 필수 기능이 아닐까 싶다. 그리고 개발 초기에 구현하는 기능이기 때문에 웹/앱 관계없이 개발자라면 꼭 알아두어야 하는 기능이 아닐까 싶다. JWT(Json Web Token) 기반의 로그인 인증 방식에 대해 나름 공부하며 얻은 지식들을 정리해본다.

JWT 기반의 로그인 인증 방식이 널리 사용되는 이유 중 하나는 Stateless(무상태성 이라고도 하는 듯)라는 특성 때문이다. 그럼 Stateless는 뭐고, 왜 좋은걸까? 그걸 알기 위해선 Stateful한 방식(세션 기반 인증)과 먼저 비교하여 이해할 필요가 있다.

 

Stateful한 세션 기반 인증 방식과 장단점

Stateful한 세션 기반 인증 방식은 전통적인 웹 애플리케이션에서 많이 사용되는 인증 방식이다. Stateful이란, 서버가 클라이언트와의 상호작용에서 상태(state)를 유지한다는 의미이다. 서버는 클라이언트와의 각 요청을 처리하면서 이전 요청에서 생성된 정보를 저장하고 관리한다. 이 정보는 보통 세션(Session)이라고 불리며, 서버는 클라이언트의 세션을 유지함으로써, 사용자가 로그인한 후에 사용자를 인식할 수 있게 된다. 세션 기반 인증은 대략 다음과 같은 절차로 작동한다.

  1. 로그인 요청 : 클라이언트가 서버에 로그인 요청을 보냄. 이 요청에는 사용자의 아이디와 패스워드와 같은 자격 증명이 포함된다.
  2. 서버에서 인증 : 서버는 이 자격 증명을 받아서 DB를 조회해 유효성을 확인한다. 유효하면 클라이언트에 대해 세션을 생성한다. 이 세션은 서버 메모리나 데이터베이스에 저장된다. 세션에는 고유한 식별자(Session ID)가 부여된다. 내가 처음 세션 개념을 알게 되었을 때에는 그냥 뭉뚱그려 개별 유저에게 부여되는 딕셔너리 형식의 자료구조라고 이해하고 사용했다.
  3. 세션ID 전달 : 서버는 이 세션 ID를 클라이언트에게 쿠키에 담아 전달한다. 이 쿠키는 클라이언트가 서버에 요청을 보낼 때마다 자동으로 포함된다.
  4. 요청 처리 : 클라이언트가 서버에 요청을 보낼 때마다, 클라이언트의 브라우저는 쿠키에 포함된 세션 ID를 서버에 보낸다. 서버는 이 세션 ID를 확인하여, 해당 사용자가 이미 인증된 상태인지, 어떤 사용자인지 등을 파악한다.
  5. 세션 유지 : 서버는 사용자가 로그아웃하거나 세션이 만료되기 전까지, 해당 세션 ID를 유지하며 클라이언트의 상태를 관리한다.

세션 기반 인증 방식을 쇼핑몰 웹사이트를 예로 들어 생각해보면, 사용자가 쇼핑몰 웹사이트에 로그인하면 서버는 사용자의 정보를 인증하고, 이 사용자에게 고유한 세션을 생성한다. 세션 ID를 'ABCD1234'라고 가정해보겠다. 서버는 이 'ABCD1234'라는 세션 ID를 서버 메모리나 DB에 저장하고, 이 세션에 사용자의 정보와 상태(ex: 장바구니에 담긴 상품 리스트)를 저장한다. 서버는 'ABCD1234'라고 하는 세션 ID를 사용자의 브라우저로 쿠키에 담아 보낸다. 사용자가 상품을 장바구니에 추가하면, 서버는 이 요청을 받아 'ABCD1234'라는 세션에 해당 상품 정보를 추가한다. 사용자가 로그아웃하면, 서버는 'ABCD1234' 세션을 삭제하고 더 이상 해당 세션을 유지하지 않는다. 이후의 요청에서는 더이상 해당 사용자를 인증된 상태로 인식하지 않게 된다.

Stateful한 세션 기반 인증의 장점은 사용자별 상태를 별도로 계속 유지하기 때문에 사용자의 특정 상태(ex: 장바구니 리스트, 사용자 설정 등)를 쉽게 관리할 수 있다. 또한, 세션 타임아웃, 세션 무효화 등의 관리 기능을 통해 사용자의 인증 상태를 관리할 수 있다.

Stateful한 세션 기반 인증의 단점으로는 서버가 각 클라이언트마다 세션을 유지해야 하므로, 사용자가 많아질수록 서버 메모리나 DB의 부하가 증가한다. 특히, 여러 서버로 확장(scale-out)할 때, 세션을 공유하는 것이 복잡해질 수 있다. 그리고 사용자의 세션이 특정 서버에 종속되기 때문에, 서버 간의 부하 분산이나 장애 처리에 어려움이 생길 수 있다.

 

Stateless한 JWT 기반 인증 방식과 장단점

그럼 이 글의 핵심인 Stateless한 방식에 대해 알아보겠다. 단어 의미대로, 서버가 클라이언트와의 각 요청 사이에 어떠한 상태도 유지하지 않는다는 의미이다. 서버는 각 요청을 독립적으로 처리하며, 이전 요청이나 이후 요청과의 연관성을 고려하지 않는다. 조금 더 자세히 들어가면, 클라이언트가 로그인하면, 서버는 JWT를 생성하여 클라이언트에게 전달한다. 이 JWT는 클라이언트가 이후 요청을 할 때마다 포함시켜 보내며, 서버는 이 토큰을 검증하여 클라이언트를 인증한다. 모든 필요한 정보는 JWT 자체에 포함되어 있기 때문에 세션이나 상태 정보를 유지할 필요가 없다.

Stateless의 장점으로는 서버를 수평적으로 쉽게 확장할 수 있다는 점이다. 서버 부하가 증가할 때 새로운 서버 인스턴스를 추가하여 트래픽을 분산시키는 것이 쉽고, 서버 코드와 인프라가 단순해질 수 있다. 또한, 클라이언트의 상태를 신경 쓰지 않고 요청을 처리할 수 있어 서비스 가동 중단 없이 쉽게 서버를 재시작하거나 배포할 수 있다는 것을 의미한다.

Stateless의 단점으로는 JWT를 클라이언트가 보관하고, 요청마다 전송하기 때문에, 만료된 토큰을 강제로 무효화하기 어렵다. 이를 해결하기 위해서 짧은 만료주기의 access token과 상대적으로 긴 만료주기의 refresh token을 같이 사용하는 방식 등으로 많이 운영된다. 또한, JWT에는 사용자에 대한 정보를 인코딩하여 저장하기 때문에, 세션 ID만 저장하는 방식보다 더 큰 크기의 데이터를 전송해야 한다. 이로 인해 네트워크 대역폭이 더 많이 사용될 수 있다.

728x90

드롭다운을 구현할 일이 있어서 정리 후 남겨본다. 드롭다운은 웹 프론트를 하다보면 꽤나 빈번히 만들 일이 생기는데, 그때그때 다른 방식으로 하다보니 나 역시도 전문성이 생긴다기보다는 그냥 그때그때의 능력껏 처리했던 것 같다. 이 기회에 정리해두고 참고하고자 한다.

 

드롭다운 구현 원리

특정 div 요소에 mouseover 이벤트가 발생하면 show 클래스리스트를 추가하여 css에서 드롭다운 메뉴를 보이도록 변경하고, mouseout 이벤트가 발생하면 show 클래스리스트를 제거하여 css에서 드롭다운 메뉴를 사라지게 하는 방식으로 구현하였다.

mouseover, mouseout이 아니라 클릭으로 구현하고 싶다면 addEventListener() 메서드의 첫 번째 argument로 click 이벤트를 활용하자.

 

javascript에서 html요소 정의하기

웹페이지에서 헤더 부분은 페이지가 변경되더라도 계속해서 쓰여야 하기 때문에, 별도의 자바스크립트에서 구현해두고 여러 페이지에서 돌려 쓰고 있던 상황이었다. 헤더메뉴 가장 우측에는 유저 id가 나오고, 유저 id에 마우스를 올리면 드롭다운 메뉴가 내려와서 POLICY, LICENSE, LOGOUT 버튼이 보여지는 드롭다운을 구현했다.

const body = document.body;
const header = $el('header', {}, [

    // 앞부분 생략
    
    $el('div.header-menu', { id: "user-id-menu" }, [
        $el('span.header-text', `Hello, ${localStorage.getItem('user-id')}`), 
        $el('img', { src: "/SVG/arrow_drop_down.svg" }),
        // user-id-menu에 mouseover, mouseout이벤트를 감지해서 드랍다운 구현
        $el('div.user-id-dropdown-submenu', { id: "user-id-dropdown-submenu" }, [
            $el('a', {
                onclick: () => { alert("[개발 중] 사용자 정책 고지"); }
            }, 'POLICY'),
            $el('a', {
                onclick: () => { alert("[개발 중] Stable Diffusion 모델, ComfyUI, 생성된 이미지의 저작권 안내"); }
            }, 'LICENSE'),
            $el('a', {
                onclick: () => { this.userLogout(); }
            }, 'LOGOUT')
        ])
    ]),
]);

body.append(header);
this.initUserIdDropdown();

initUserIdDropdown() {
    const userIdMenu = document.getElementById('user-id-menu');

    function showUserIdMenuDropdown() {
        const userIdDropdownSubmenu = document.getElementById('user-id-dropdown-submenu');
        userIdDropdownSubmenu.classList.add('show');
    }

    function hideUserIdMenuDropdown() {
        const userIdDropdownSubmenu = document.getElementById('user-id-dropdown-submenu');
        userIdDropdownSubmenu.classList.remove('show');
    }

    userIdMenu.addEventListener('mouseover', showUserIdMenuDropdown);
    userIdMenu.addEventListener('mouseout', hideUserIdMenuDropdown);
}

export function $el(tag, propsOrChildren, children) {
	const split = tag.split(".");
	const element = document.createElement(split.shift());
	if (split.length > 0) {
		element.classList.add(...split);
	}

	if (propsOrChildren) {
		if (typeof propsOrChildren === "string") {
			propsOrChildren = { textContent: propsOrChildren };
		} else if (propsOrChildren instanceof Element) {
			propsOrChildren = [propsOrChildren];
		}
		if (Array.isArray(propsOrChildren)) {
			element.append(...propsOrChildren);
		} else {
			const {parent, $: cb, dataset, style} = propsOrChildren;
			delete propsOrChildren.parent;
			delete propsOrChildren.$;
			delete propsOrChildren.dataset;
			delete propsOrChildren.style;

			if (Object.hasOwn(propsOrChildren, "for")) {
				element.setAttribute("for", propsOrChildren.for)
			}
			if (style) {
				Object.assign(element.style, style);
			}
			if (dataset) {
				Object.assign(element.dataset, dataset);
			}

			Object.assign(element, propsOrChildren);
			if (children) {
				element.append(...(children instanceof Array ? children : [children]));
			}
			if (parent) {
				parent.append(element);
			}
			if (cb) {
				cb(element);
			}
		}
	}
	return element;
}

위의 코드에서 html 요소 정의에 사용된 $el 메서드는 오픈소스 프로젝트인 ComfyUI 코드를 분석하다 발견했는데, 이후에도 꽤나 잘 사용하고 있다.

 

css 코드

.header-menu {
  display: flex;
  margin-right: 15px;
  font-weight: bold;
  color: rgba(255, 255, 255, 1);
  line-height: 30px;
  font-weight: 700;
  font-size: 13px;

  cursor: pointer;
}

.header-menu a {
  color: inherit;
  text-decoration: none;
  display: inline-block;
  width: 100%;
  height: 100%;
}

.header-text:hover {
  background-size: 100% 2px;
  background-image: linear-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 1));
  background-repeat: no-repeat;
  background-position: left 0 bottom 0;
}

.header-text {
  padding-bottom: 2px;
  background-size: 0 2px;
}

.user-id-dropdown-submenu {
  display: none; /* 기본적으로 숨김 */
  position: absolute;
  top: 41px;

  background-color: rgba(0, 0, 0, 0.6);
  color: #333333;
  z-index: 1;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.7s ease;

  padding-top: 10px;
  padding-right: 5px;
  padding-left: 5px;
}

.user-id-dropdown-submenu a {
  color: rgba(255, 255, 255, 1);
  text-decoration: none;
  display: block;

  padding-right: 20px;
  padding-left: 20px;
  margin-bottom: 5px;
}

.user-id-dropdown-submenu a:hover {
  background-color: rgba(100, 100, 100, 0.6);
}

.user-id-dropdown-submenu.show {
  display: block;
  max-height: 200px;
}

기본적으로 user-id-dropdown-submenu가 display: none;으로 보이지 않다가, 자바스크립트에서 동적으로 show 클래스리스트를 추가하면 보이도록 만들어 두었다. 애니메이션을 넣고싶은데 이건 나중에 시간 날때 해보도록 하자.

728x90

공유받은 프로젝트 내에서 pip install -r requirements.txt 를 이용해서 모듈을 설치했는데 main.py를 실행시키면 해당 모듈을 찾을 수 없다고 나오고, 결국 하나하나 pip install로 설치해야 하는 문제 발생

728x90

'🔨 개발 > 📚 개발지식모음집' 카테고리의 다른 글

파이썬 람다 함수 사용법 정리  (2) 2024.11.04
YAML 파일이란?  (1) 2024.09.13
20240808  (0) 2024.08.08
20240807  (0) 2024.08.07
20240806  (0) 2024.08.06

CocoaPods: iOS 프로젝트의 필수 라이브러리 관리 도구

swift와는 직접적으로 연관되어 있지는 않지만, 카카오톡 소셜 로그인 기능을 구현하다가 필요해서 글을 남긴다.

CocoaPods는 iOS 및 macOS 개발에서 필수적인 라이브러리 관리 도구이다. 수천 개의 오픈소스 라이브러리를 손쉽게 프로젝트에 통합하고 관리할 수 있게 한다. (python에서의 pip라고 생각하면 좋을 지도?) 이번 포스팅에서는 Cocoapods의 개념, 설치 방법, 그리고 실제로 프로젝트에서 사용하는 방법을 자세히 설명해 보겠다.

 

1. CocoaPods란?

CocoaPods는 Xcode 프로젝트에 외부 라이브러리를 쉽게 통합하고 관리할 수 있게 해주는 의존성 관리 도구이다. 오픈 소스 라이브러리뿐만 아니라 사내에서 개발한 라이브러리도 쉽게 추가, 업데이트, 삭제할 수 있으며, 라이브러리 간의 의존성을 자동으로 해결해준다.

 

2. CocoaPods 설치

CocaPods를 사용하기 위해서는 먼저 설치가 필요하다. 설치는 RubyGems를 통해 간단하게 할 수 있다. 먼저 터미널을 열고, 다음 명령어로 CocoaPods를 설치한다. 만약 RubyGems 버전이 낮다면 버전을 올린 뒤 설치한다.

sudo gem install drb -v 버전
sudo gem install cocoapods

설치가 완료되면 CocoaPods 버전을 확인하여 제대로 설치되었는지 확인할 수 있다.

pod --version

 

3. CocoaPods 설정

CocoaPods를 설치하고 나서 Xcode 프로젝트에 설정하는 방법을 알아보자. 먼저 Xcode 프로젝트가 있는 디렉토리로 이동한다. 그리고 Podfile을 생성해준다. Podfile은 프로젝트에서 사용할 라이브러리 목록을 정의하는 파일로, bash에서

pod init

으로 생성할 수 있다. 꼭 /project 에서 생성해주자. 그리고 뭐든 편집기를 이용하여 Podfile을 수정해주자. Podfile을 열어 사용할 라이브러리를 추가한다. 나는 kakao developers에서 안내하는 대로 작성했다.

카카오 로그인을 위한 Podfile 설정
카카오 로그인을 위한 Podfile 설정

안내에는 이것저것 많이 알려줬지만, 사실 필요한건 KakaoSDKCommon과 KakaoSDKAuth, KakaoSDKUser 인듯하다. Podfile을 수정했다면, 다음 명령어를 실행하여 라이브러리를 설치하자.

pod install

이 명령어는 'Podfile'에 명시된 모든 라이브러리를 다운로드하고, Xcode 프로젝트를 위한 새로운 '.xcworkspace'파일을 생성한다. 앞으로는 '.xcodeproj'파일이 아니라, '.xcworkspace'파일을 사용해야 한다. 이 파일을 사용하여 Xcode에서 프로젝트를 열고 작업을 시작한다.

 

4. CocoaPods 사용 시 유의사항

Pod 업데이트: 라이브러리를 업데이트하려면 'Podfile'을 수정한 후, 'pod update'라는 명령어를 실행하여 업데이트 할 수 있다. 특정 라이브러리만 업데이트하려면 'pod update 라이브러리'와 같이 라이브러리 이름을 명시할 수 있다.

Pod 삭제: 사용하지 않는 라이브러리를 삭제하려면 'Podfile'에서 해당 라이브러리 항목을 제거한 후, 'pod install' 명령어를 실행한다.

Pod 버전 고정: 특정 버전의 라이브러리를 유지하려면 'Podfile'에서 버전을 명시해두는 것이 좋다. 그러면 업데이트 시에도 이 버전이 유지된다.

728x90

이전글: 2024.08.20 - [🔨 개발/📱 swift] - swift를 이용한 iOS 앱 개발 입문 3: 'IBOutlet'과 'IBActon'

UITextField를 사용해서 유저로부터 텍스트 입력을 받는 기능을 만들게 되었다. 아마 거의 모든 앱에서 사용될 기능이 아닐까 싶은데, 생각보다 간단하면서도 그와중에 고군분투의 흔적이 있어 기록으로 남겨본다.

 

1. storyboard에서 UITextField 생성하고 배치하기

Text Field를 storyboard에 추가
Text Field를 storyboard에 추가

필요한 UI 요소를 드래그앤드롭으로 storyboard에 추가하고 constraints를 설정하여 원하는 위치에 예쁘게 배치해준다. 이제는 기계적으로 하는 작업이 되어버렸다. 그리고 storyboard 옆에다 ViewController를 열어 control+드래그로 IBOutlet을 연결한다. IBOutlet의 사용방법을 모른다면 이전 글을 참고하자. commentText라는 변수명으로 ViewController 파일에서 댓글 텍스트 영역을 동적으로 접근하고자 한다.

@IBOutlet weak var commentText: UITextField! // 댓글 텍스트 영역


2. 각종 꾸미기 옵션들(borderWidth, borderColor, placeholder)

이제 commentText를 예쁘게 꾸며주는 몇 가지 작업들을 거쳐본다. viewDidLoad() 메서드 내부에 borderwidth, borderColor를 설정하는데, borderColor라는 변수는 휴대폰 기기의 설정이 다크 모드이면 흰색, 라이트 모드이면 검은색으로 주었다. 그리고 placeholder 역시 UITextField가 비활성화 된 상태와 활성화 된 상태를 다르게 설정하였다. 매번 isEnabled를 바꿔줄때마다 updateCommentPlaceholder() 메서드를 수동으로 호출해야 하는지는 의문이지만, 추후에 공부하기로 하고 일단은 번거로워도 그냥 호출한다.

@IBOutlet weak var commentDiv: UIView! // 댓글 영역
@IBOutlet weak var commentImg: UIImageView! // 댓글 흑백 아이콘
@IBOutlet weak var commentText: UITextField! // 댓글 텍스트 영역

override func viewDidLoad() {
	// 생략
    
    let borderColor = UIColor { traitCollection in
            return traitCollection.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
        }
    
    // 댓글 텍스트 영역의 border 옵션 설정
    commentText.layer.borderWidth = 1.0
    commentText.layer.borderColor = borderColor.cgColor
    
    commentText.isEnabled = false // 최초에는 비활성화 된 상태로 시작
    self.updateCommentPlaceholder()
}

func updateCommentPlaceholder() {
    if commentText.isEnabled {
        commentText.placeholder = "00님의 생각을 공유해주세요."
    } else {
        commentText.placeholder = "선택지를 고른 후 댓글을 남길 수 있어요."
    }
}

이제 여기까지 왔으면 꽤 멀쩡해보이는 화면이 만들어 진 듯하다.

 

3. 가상 키보드 높이를 감지하여 위치 이동하기

UITextField에 별도의 IBAction을 지정하지 않아도 기본적으로 터치 시, 아이폰의 가상 키보드를 열어주는 역할을 하나보다. 그런데 문제는, 화면의 최하단(정확히 말하면 safearea의 최하단)에 고정되어 있는 UITextField가 가상 키보드가 나타나면서 같이 따라 올라오지 않아 눈에 보이지 않는다는 것이다. 이를 NotificationCenter를 이용해서 해결한 방법을 공유하고자 한다.

override func viewDidLoad() {
    super.viewDidLoad()

    // 생략

    // 댓글 영역 배경색 설정
    commentDiv.backgroundColor = UIColor { traitCollection in
            return traitCollection.userInterfaceStyle == .dark ? UIColor.black : UIColor.white
    }
    // 댓글 영역 그림자 설정
    self.addShadowToCommentDiv(view: commentDiv)

    // 댓글 텍스트 영역의 border 옵션 설정
    commentText.layer.borderWidth = 1.0
    commentText.layer.borderColor = borderColor.cgColor

    // 키보드 프레임 변경 노티피케이션 등록
    NotificationCenter.default.addObserver(self, selector: #selector(updateKeyboardFrame(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
            
    commentText.delegate = self // 텍스트 필드 델리게이트 설정

}

@objc func updateKeyboardFrame(_ notification: NSNotification) {
    if let userInfo = notification.userInfo,
       let keyboardFrameEnd = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
       let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
       let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber {

        // Safe Area Insets 가져오기
        let bottomSafeAreaInset = view.safeAreaInsets.bottom

        // 키보드 높이에서 Safe Area를 고려한 높이 계산
        let adjustedKeyboardHeight = keyboardFrameEnd.height - bottomSafeAreaInset

        // 키보드 애니메이션의 곡선 및 지속 시간 가져오기
        let animationCurveRaw = animationCurveRawNSN.uintValue
        let animationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw << 16)

        // commentDiv의 위치를 키보드 높이에 맞춰 애니메이션으로 변경
        UIView.animate(withDuration: animationDuration,
                       delay: 0,
                       options: animationOptions,
                       animations: {
                           if keyboardFrameEnd.origin.y >= UIScreen.main.bounds.height {
                               // 키보드가 내려간 상태면 원래 위치로 복원
                               self.commentDiv.transform = .identity
                           } else {
                               // 키보드가 나타난 상태면 Safe Area를 고려하여 올리기
                               self.commentDiv.transform = CGAffineTransform(translationX: 0, y: -adjustedKeyboardHeight)
                           }
                       }, completion: nil)
    }
}

먼저 Notification 등록은, 키보드 프레임이 변결될 때 발생하는 노티피케이션으로, 키보드의 크기나 위치가 변경될 때(ex: 키보드가 올라오거나 내려갈 때, 혹은 회전할 때 등)에 해당 이벤트를 감지할 수 있다. UIResponder.keyboardWillChangeFrameNotification 외에도 UIResponder.keyboardWillShowNotification과 UIResponder.keyboardWillHideNotification을 사용하는 방법도 있지만, 이렇게 해보니 가상키보드의 영역이 입력 도중 변화할 때(ex. 한국어 -> 일본어로 언어 전환을 하면 일본어 키보드가 한자변환 추천 영역 때문에 한국어나 영어보다 사이즈가 크다. 또한 이모지 입력시에도 키보드 크기가 변화한다)를 감지하지 못하는 문제가 있었다.

가상키보드 영역의 높이를 감지해서 IBOutlet으로 연결한 UIView(또는 UITextField)의 위치를 updateKeyboardFrame 이라는 메서드에서 변경해준다. 이때, 단순히 키보드 높이만큼 translate하는 것이 아니라, SafeAreaInset을 고려해서 높이를 조정한다. 그냥 위로 올려버리면 텍스트 입력 필드 아래에 빈 공간이 생기게 된다. safearea에 대해 조금 더 공부하고 별도의 포스팅으로 올려야겠다.

 

4. 입력 완료 이후 가상 키보드 내리기

이제 유저가 텍스트 입력을 완료하면, 가상 키보드를 적절한 방식으로 없애 주어야 하는데, 이 부분이 잘 처리되지 못한 앱을 종종 본 적이 있다. 나는 우선 유저가 UITextField 외의 다른 영역을 터치하면 키보드가 사라지는 방식을 사용했다.

// 화면의 빈 공간을 터치하면 호출되는 메서드
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    // 현재 화면에서 키보드를 숨김
    self.commentText.endEditing(true)
}

이 외에도 아래로 스와이프를 통해 키보드를 내리는 카카오톡, 리턴(엔터)키 입력으로 텍스트를 전송하며 키보드를 사라지게 하는 방식 등 다양한 방식들이 사용되고 있다. 스와이프로 키보드를 내리는 방식은 구현에 도전해봐야겠다.

 

728x90

+ Recent posts