티스토리 뷰

서버에 파일을 저장하고 이 파일을 불러와서 이미지로 사용하는 view를 만들어 보려 한다. 먼저 이미지 파일이 서버에 어떤 식으로 저장되어 있는지 알 필요가 있는데, 인터넷에서 아무 사진이나 검색한 뒤 브라우저에서 정확히 이미지만 보일 때 까지 접근하면 주소창에 이미지의 경로 주소가 나온다.

 

뭔가 암호처럼 보이지만 결국 디렉토리에서 .jpg파일의 경로가 적혀있다.

 

사실 이것 또한 방법의 차이일 뿐 어떤게 정답이라고 할 수는 없는 부분인것 같다. 위처럼 만들면 데이터 베이스에는 파일이 아니라 파일이 저장된 경로를 저장하고, 실제 파일은 서버가 접근할 수 있는 파일 디렉토리에 저장하는 방법을 채택할 수 있다.

현재 서버와 데이터베이스는 데스크탑에서, IOS 프로젝트는 맥북에서 실행하고 있는데, 예상할 수 있는 이미지 파일 저장 및 로드 구조는 대략 이렇게 된다.

 

정답은 없다.

 

데이터 베이스는 D:/profile/ 다음부터 오는 파일 이름이 저장되어 있고, 클라이언트는 프로필 데이터를 수신할 때 이 경로를 알 수 있게 된다. 그리고 그 경로를 다시 요청하면 서버는 이미지 파일을 찾아서 클라이언트에게 다시 전송한다.\

 

 

 

서버

마찬가지로 서버는 주된 내용이 아니기 때문에 빠르게 넘어간다.

먼저 테스트에 사용할 사진을 하나 준비해서 D:/profile/ 경로에 저장한다. 그리고 DB에 접속해서 원래 저장하고 있던 profileImagePath를 수정한다. 이 때 만들기 나름이지만 profile이미지는 profile 밑에 저장될 것이기 때문에 D:/profile/을 생략한 실제 파일 이름만 저장했다.

 

경우에 따라서 추가적인 경로를 저장할 수도 있다.

 

데이터베이스 준비는 끝났다. 서버 코드로 돌아가서 이미지 요청을 처리할 수 있는 함수를 만들자. 서버 개발에서는 이러한 함수를 servlet이라고 부르는 것 같다.

 

@GetMapping("/profile/{file}")
public void GetProfileImage(@PathVariable("file")String fileName) {
	try {
		FileInputStream fileStream = new FileInputStream("d:/profile/" + fileName);
		response.setHeader("Content-Type", "image/jpeg");
		OutputStream outStream = response.getOutputStream();
		var buffer = new byte[1024];
		int length = 0;
		int temp;
		while ((temp = fileStream.read(buffer)) > 0) {
			outStream.write(buffer, 0, temp);
			length += temp;
		}
		response.setHeader("Content-Length", Integer.toString(length));
	} catch (FileNotFoundException e) {
		System.out.print(e);
	} catch (IOException e) {
		System.out.print(e);
	}
}

 

@PathVariable은 요청 url에서 {}부분에 입력된 값을 매핑해주는 annotation으로 파일 이름을 입력하면 그 파일을 열어서 response의 outStream에 적어서 다시 클라이언트로 response한다.

포스트맨을 열어서 파일 경로로 요청해보자.

 

만족

 

 

 

UIImage

UIImage는 다양한 방법으로 생성할 수 있지만 Data로도 생성할 수 있다. 위에서 만든 서버로 이미지를 요청하면 이미지로 보이지만 사실은 byte배열을 보낼 뿐이기 때문에 이 byte를 사용해서(Data는 byte array다.)UIImage를 만들 수 있을 것 같다. 심지어 Data는 URL로 만들 수 있다.

 

let url = URL(string: "server-url/profile/image.jpg")!
if let data = try? Data(contetnOf: url) {
	profileImage.image = UIImage(data: data)
}

 

물론 이렇게 만들어도 실제로 동작하기는 하지만 치명적인 문제가 있다. url로 Data를 생성하는 과정이 비동기적이지 않다는 점이다. 통신도 빠르고 서버도 빠르면 크게 느리게 느껴지지는 않겠지만 서버에서 이미지 요청을 받았을 때 sleep을 걸어서 멈춰보면 응답을 받을 때 까지 프로그램이 멈춘다.

github에 누군가 이 불편함을 수정한 비동기 UIImageView, SDWebImageView라는 프로젝트를 만들어서 공유한 것이 있다. view를 만들 때 url과 placeholder로 사용할 UIImage를 넘겨주면 비동기적으로 알아서 url로부터 받은 이미지를 업데이트 해 주는 view를 만든 것 같다. 이걸 사용해도 되지만 그렇게 어려워보이지 않으니 직접 만들어보자.

새로운 파일을 만들고 UIImageView를 상속받도록 한다. 그리고 똑같이 URL과 UIImage를 받는 생성자를 만든다. 전달받은 Data를 가지고 UIImage를 만들어서 적용하는 fatch라는 함수도 작성한다.

 

class AsyncWebImage : UIImageView {
	init(url optionalURL: URL?, placeholder optionalPlaceholder: UIImage?) {
		super.init(frame: .zero)
		
		self.image = placeholder

		if let url = optionalURL else {
			updateURL(url)
		}
	}

	func fatch(_ data: Data) {
		self.image = UIImage(data: data)
	}
}

 

optional 변수들을 guard로 unwrap하는 과정에서 두 변수의 이름이 같아도 컴파일이 되지만 아무래도 너무 헷갈려서 optional 변수들은 optional이라는 접두어를 추가했다. 분명히 같은 함수 안에있는 url을 사용하는데 어디에서는 URL?이고 어디에서는 URL로 unwrap되어서 처리되는게 영 적응이 안된다.

updateURL은 request를 전송하는데, 미리 만들어 둔 Communicator 같은 외부 클래스에서 처리하도록 전달하는것도 괜찮지만 그렇게 큰 작업이 아니기 때문에 직접 처리하도록 만들었다.

 

// AsyncWebImage
func updateURL(url: URL) {
	var request: URLRequest(url: url)
	request.httpMethod = "GET"

	let session = URLSession.shared
	session.dataTask(request: request, completionHandler: {data, response, error in
		
		guard let data = data else {
			print("nil response data")
			return 
		}

		DispatchQueue.main.async {
			self.fatch(data)
		}
	}).resume()
}

 

이제 프로필 화면에서 사용하는 프로필 이미지를 새로 만든 AsyncWebImage로 바꾸고 적절한 부분에 updateURL을 요청하면 될 것 같다. 처음 viewDidLoad에서 생성할 때는 url은 nil, placeholder에는 원하는 로컬 이미지를 입력한다.

 

profileImage = AsyncWebImage(url: nil, placeholder: UIImage(named: "person.fill"))

 

이전 포스팅에서 끌어내려서 새로고침을 하면 그 때 서버의 데이터를 받아오기 때문에 서버 데이터를 수신했을 때 실행되는 needToRefresh로 이동해서 전달받은 data로 url을 만들어서 updateURL을 호출해서 이미지를 요청할 수 있도록 한다.

 

// refresh delegate
func needToRefresh(_ data: Data) {
	// make 'serverData' by json decode 'data'
	let urlStr = serverURL + "/profile/" + serverData.profileImagePath
	if let url = URL(string: urlStr) {
		profileImage.updateURL(url: url)
	}

	// ....
}

 

 

 

Result

이전 포스팅과 마찬가지로 처음에는 로컬 이미지로 초기화되어 있지만 끌어내려서 서버 데이터를 요청하면 서버에 저장되어 있던 새로운 사진과 서버 데이터로 화면이 바뀐다.

 

빠밤

 

서버와 직접 연동하니까 클라이언트 작업만 할 때와는 또다른 재미가 있다. 여러개의 프로그램이 여러대의 컴퓨터에서 각각 돌아가는데, 이 프로그램 끼리 서로 통신을 하면서 데이터를 주고받고 그 결과가 화면에 그려질 때 느껴지는 쾌감이 아주 좋다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함