1. 개요
길찾기 별 QR코드 이미지 제공을 위해서 이미지 저장 기능을 구현해야했다.
2. PreSignedUrl
3. AWS S3 설정
루트 권한의 AccessKey를 만들기 위해서는 IAM 페이지 접속 → 우측 상단 프로필 → 보안 자격 증명 → 진행
위와 같이 하면 되는데 이는 보안에 취약하고 권장하지 않음
3.1 IAM 사용자 만들기
루트 권한의 AccessKey 대신 S3에 접근 가능한 사용자를 만들고
해당 사용자의 AccessKey를 사용하자.
정책에서 S3FullAccess 추가 후 사용자 생성하면 된다.
3.2 버킷 CORS 설정
내가 사용하려는 버킷 접속 → 권한 클릭 → 아래로 쭉 내리면 마지막에 CORS 설정 나옴
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["*"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]
3.3 버킷 정책 권한 설정
{
"Version": "2012-10-17",
"Id": "Policy1746266864251",
"Statement": [
{
"Sid": "Stmt1746266861724",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::gachonlife-s3-bucket/*"
}
]
}
버킷 정책을 편집할 권한이 없거나 버킷 정책이 퍼블릭 액세스 차단 설정과 충돌하는 퍼블릭 액세스 수준을 부여합니다. 버킷 정책을 편집하려면 s3:putbucketpolicy 권한이 필요합니다.
이러면 이제 다음과 같은 에러가 뜨는데 이는 버킷 설정인 퍼블릭 액세스 차단(버킷 설정)을 아예 막아놨기 때문이다.
이는 퍼블릭 액세스 차단 설정을 없애면 된다. (보안상 좋지 않다..) 또는 IAM으로 접근!
AccessKey와 비밀키는 따로 저장해두자!
4. Spring Boot 코드
4.1 의존성 설치
implementation 'software.amazon.awssdk:s3:2.29.46'
4.2 AwsS3Config
@Configuration
public class AwsS3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secreteKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public S3Presigner s3Presigner() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secreteKey);
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}
4.3 ImageUtil
@Component
@RequiredArgsConstructor
public class ImageUtil {
private final S3Presigner s3Presigner;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String generateUrl(String fileName) {
String key = generateKey(fileName);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
PutObjectPresignRequest request = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.putObjectRequest(putObjectRequest)
.build();
PresignedPutObjectRequest presignedUrlRequest = s3Presigner.presignPutObject(request);
return presignedUrlRequest.url().toString();
}
private String generateKey(String fileName) {
String key = UUID.randomUUID().toString();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
return key + "." + extension;
}
}
경로가 겹치지 않도록 UUID 설정을 해주고, fileName으로부터 확장자를 가져온다.
이를 통해 bucket에 대한 객체를 만들어주고, 해당 객체를 PutObjectPresgienRequest 빌더에 넣어서 PresignedUrl을 가져온다.
4.4 ImageController
@RestController
@RequiredArgsConstructor
public class ImageController {
private final ImageUtil imageUtil;
@GetMapping("/image/presigned-url")
public ApiResponse<String> getPreSignedUrl(@RequestParam String fileName) {
String preSignedUrl = imageUtil.generateUrl(fileName);
return ApiResponse.response(HttpStatus.OK, PRESIGNED_URL_GENERATE_SUCCESS.getMessage(), preSignedUrl);
}
}
단순하게 ImageUtil을 사용해서 PreSignedUrl을 응답해주는 컨트롤러이다.
5. React 코드
이미지 업로드 테스트를 위해 GPT에게 요청했다.
더보기
import React, { useState } from 'react';
import axios from 'axios';
import './ImageUploader.css'; // 스타일 분리 (아래에 CSS 제공)
const ImageUploader = () => {
const [imageUrl, setImageUrl] = useState(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState('');
const getPresignedUrl = async (fileName) => {
const response = await axios.get('http://110.15.135.250:8000/movement-service/image/presigned-url', {
params: { fileName },
headers: {
// 실제 토큰 사용하기.
Authorization: `
Bearer Token값`,
},
});
return response.data.data;
};
const uploadImageToS3 = async (file, presignedUrl) => {
await axios.put(presignedUrl, file, {
headers: {
'Content-Type': file.type,
},
});
return presignedUrl.split('?')[0]; // 실제 접근 가능한 S3 URL
};
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
setError('');
setUploading(true);
try {
const presignedUrl = await getPresignedUrl(file.name);
console.log("presignedUrl: "+presignedUrl)
const uploadedUrl = await uploadImageToS3(file, presignedUrl);
setImageUrl(uploadedUrl);
console.log(uploadedUrl)
} catch (err) {
console.error(err);
setError('이미지 업로드에 실패했습니다.');
} finally {
setUploading(false);
}
};
return (
<div className="uploader-container">
<h2>S3 이미지 업로더</h2>
<label className="file-label">
파일 선택
<input type="file" accept="image/*" onChange={handleFileChange} />
</label>
{uploading && <p className="loading">업로드 중...</p>}
{error && <p className="error">{error}</p>}
{imageUrl && (
<div className="preview">
<p>업로드된 이미지:</p>
<img src={imageUrl} alt="uploaded" />
</div>
)}
</div>
);
};
export default ImageUploader;
이와중에 이쁘게 보이기 위해서 css파일도 만들어달라했다 ㅋㅋ
더보기
.uploader-container {
max-width: 500px;
margin: 40px auto;
padding: 24px;
border-radius: 12px;
background-color: #f7f7f7;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
text-align: center;
font-family: sans-serif;
}
.file-label {
display: inline-block;
background-color: #3498db;
color: white;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
margin-top: 16px;
font-weight: bold;
transition: background-color 0.2s ease;
}
.file-label input {
display: none;
}
.file-label:hover {
background-color: #2980b9;
}
.loading {
color: #e67e22;
margin-top: 10px;
}
.error {
color: red;
margin-top: 10px;
}
.preview {
margin-top: 20px;
}
.preview img {
width: 100%;
max-height: 300px;
object-fit: contain;
border-radius: 8px;
border: 1px solid #ccc;
}
'프로젝트에서 일어난 일' 카테고리의 다른 글
디스코드 뮤직 봇 개발 및 배포까지 (1) | 2025.04.26 |
---|---|
Spring Cloud Gateway에서 인증/인가 구현하기 (0) | 2025.04.25 |
DataGrip에서 SSH 터널링으로 MySQL Container 접속하기 (0) | 2025.04.18 |
라즈베리파이 + MSA(Spring Cloud) + CI/CD 배포 (1) | 2025.04.17 |
다수 데이터 Insert 시 성능 개선하기(37.06% 개선) (1) | 2025.04.16 |