<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>sh0r.dev</title>
    <link>https://molasses-0.tistory.com/</link>
    <description>프로그래밍을 하며 생기는 에러나 트러블슈팅에 대한 내용들입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 17:23:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dalbodeule</managingEditor>
    <image>
      <title>sh0r.dev</title>
      <url>https://tistory1.daumcdn.net/tistory/7060792/attach/7c03026360ea4be0b3417b97bddf0aa9</url>
      <link>https://molasses-0.tistory.com</link>
    </image>
    <item>
      <title>Play Integrity API/iOS AppAttest 사용하기 (4. 클라이언트 사이드)</title>
      <link>https://molasses-0.tistory.com/19</link>
      <description>&lt;h1&gt;서론&lt;/h1&gt;
&lt;p&gt;졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.&lt;br&gt;이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.&lt;/p&gt;
&lt;h1&gt;본론&lt;/h1&gt;
&lt;p&gt;React Native의 경우, 여러가지 설정이 복잡하게 들어가있다. 우선 다음과 같은 flag들을 추가해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;expo&amp;quot;: {
    &amp;quot;ios&amp;quot;: {
      &amp;quot;entitlements&amp;quot;: {
        &amp;quot;com.apple.developer.devicecheck.appattest-environment&amp;quot;: &amp;quot;development&amp;quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 코드는 &lt;code&gt;app.json&lt;/code&gt;에 추가되어야 하는 파트이다.&lt;br&gt;iOS APP을 빌드할 때 AppAttest 환경을 어떤 것을 사용할 것이냐에 대한 필드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const getDeviceId = async() =&amp;gt; {
    return await DeviceInfo.getUniqueId()
}

const getBundleId = () =&amp;gt; {
    return DeviceInfo.getBundleId();
}

export const requestChallenge = async () =&amp;gt; {
    const deviceId = await getDeviceId()

    try {
        const response = await axios.post&amp;lt;RequestChallengeResponse&amp;gt;(`${apiUrl}/api/integrity/challenge`,
            { deviceId },
            {}
        )

        const data = response.data;
        if (response.status !== 200) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const expDate = new Date()
        expDate.setMinutes(expDate.getMinutes() + data.expiresInMinutes)

        await AsyncStorage.setItem(&amp;#39;integrityChallenge&amp;#39;, data.challenge);
        await AsyncStorage.setItem(&amp;#39;integrityChallengeExp&amp;#39;, expDate.toISOString())

        return data.challenge;
    } catch (error: any) {
        console.error(&amp;#39;Failed to get challenge: &amp;#39; + error.message);
        throw error;
    }
}

export const verifyDeviceIntegrity = async () =&amp;gt; {
    let challenge = await AsyncStorage.getItem(&amp;#39;integrityChallenge&amp;#39;);
    const expDate = await AsyncStorage.getItem(&amp;#39;integrityChallengeExp&amp;#39;);
    const deviceId = await getDeviceId()
    const platform = Platform.OS;
    const bundleId = getBundleId();

    if (!challenge || (expDate &amp;amp;&amp;amp; new Date(expDate).getTime() &amp;lt; Date.now())) {
        challenge = await requestChallenge();
    }

    try {
        const googleCloudProject: number = parseInt(EXPO_PUBLIC_GOOGLE_CLOUD_PROJECT);

        let attestation = null
        let keyId = null

        try {
            if (platform == &amp;quot;ios&amp;quot;) {
                if(!Integrity.isSupported())
                    throw Error(&amp;quot;Integrity is not supported on this device&amp;quot;);
                attestation = await Integrity.attestKey(challenge);
                keyId = await SecureStore.getItemAsync(
                    SECURE_STORAGE_KEYS.INTEGRITY_KEY_IDENTIFIER,
                )
            } else if (platform == &amp;quot;android&amp;quot;) {
                attestation = await Integrity.attestKey(challenge, googleCloudProject);
            } else throw Error(&amp;quot;Platform not supported&amp;quot;);
        } catch (error) { throw error }

        console.log({
            platform,
            attestation,
            bundleId,
            challenge,
            deviceId,
            keyId
        })

        const response = await axios.post&amp;lt;VerifyDeviceIntegrityResponse&amp;gt;(`${apiUrl}/api/integrity/verify`, {
            platform,
            attestation,
            bundleId,
            challenge,
            deviceId,
            keyId
        }, {

        });

        const data = response.data
        if (response.status !== 200 || !data.isValid) {
            throw new Error(`HTTP error! status: ${response.status} / ${data.message} / ${data.details ? JSON.stringify(data.details) : &amp;#39;&amp;#39;}`);
        }

        if (data.isValid) {
            await AsyncStorage.removeItem(&amp;#39;integrityChallenge&amp;#39;);
            await AsyncStorage.removeItem(&amp;#39;integrityChallengeExp&amp;#39;);
        }
        return data;
    } catch (error: any) {
        console.error(`Failed to verify integrity: ${error.message} / ${error.details ? JSON.stringify(error.details) : &amp;#39;&amp;#39;}`);
        throw error;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다음은 Android, iOS 모두 검증하는 클라이언트 사이드 로직이다.&lt;/p&gt;
&lt;p&gt;라이브러리는 본인이 직접 수정한 라이브러리를 사용하길 권장한다. expo SDK 53에서도 사용가능함을 검증한 라이브러리이다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/20203153/expo-app-integrity&quot;&gt;github&lt;/a&gt; / &lt;a href=&quot;https://www.npmjs.com/package/@dalbodeule/expo-app-integrity&quot;&gt;npmjs&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p&gt;본인이 여러 생고생을 같이 했기 때문에 빨리 이 글을 찾아 광명을 누리길 바란다...&lt;/p&gt;</description>
      <category>TypeScript/React</category>
      <category>Attest</category>
      <category>integrity</category>
      <category>ios appattest</category>
      <category>play integrity service</category>
      <category>reactnative</category>
      <category>무결성 검증</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/19</guid>
      <comments>https://molasses-0.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 12 May 2025 23:29:44 +0900</pubDate>
    </item>
    <item>
      <title>Springboot에서 AppAttest API 사용하기 (3. iOS AppAttest)</title>
      <link>https://molasses-0.tistory.com/18</link>
      <description>&lt;h1&gt;서론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.&lt;br /&gt;이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.&lt;/p&gt;
&lt;h1&gt;본론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS AppAttest도 앱의 변조 여부를 파악하는 역할을 한다. 하지만 iOS의 경우 키 복호화 등 절차가 너무 복잡해 라이브러리를 사용하여 문제를 해결하였다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// implementation(&quot;ch.veehait.devicecheck:devicecheck-appattest:0.9.6&quot;)

lateinit var attest: AppleAppAttest

init {
    attest = AppleAppAttest(
        app = App(teamId, packageName),
        appleAppAttestEnvironment = AppleAppAttestEnvironment.PRODUCTION
    )
}

fun verifyIosAppAttest(
    attestation: String,
    keyId: String?,
    challenge: String
): IntegrityVerificationResponse {
    try {
        val validator = attest.createAttestationValidator()

        val result = validator.validate(
            Base64.getDecoder().decode(attestation),
            keyId ?: &quot;&quot;,
            Base64.getDecoder().decode(challenge)
        )
        // 라이브러리의 validate 메소드는 검증 실패 시 AttestationException을 throw 합니다.
        // 여기까지 도달했다는 것은 검증에 성공했다는 의미입니다.
        return IntegrityVerificationResponse(
            isValid = true,
            message = &quot;iOS app attestation verified successfully&quot;
            // 필요한 경우 'result' 객체에서 추가 정보를 추출하여 details에 포함
        )
    } catch (e: AttestationException) { // &amp;lt;-- 라이브러리의 특정 예외를 캐치
        // Attestation 검증 자체에서 발생한 오류
        logger.error(&quot;App attestation validation failed: ${e.message}&quot;, e)
        return IntegrityVerificationResponse(
            isValid = false,
            message = &quot;App attestation validation failed: ${e.message}&quot;,
            details = mapOf(&quot;errorType&quot; to e.javaClass.simpleName, &quot;validationError&quot; to (e.message ?: &quot;unknown&quot;)) // 예외 메시지 포함
        )
    } catch (e: Exception) { // 그 외 예상치 못한 다른 오류
        logger.error(&quot;Unexpected error during iOS app attestation verification&quot;, e)
        return IntegrityVerificationResponse(
            isValid = false,
            message = &quot;Unexpected error verifying iOS attestation: ${e.localizedMessage ?: e.message}&quot;,
            details = mapOf(&quot;errorType&quot; to e.javaClass.simpleName)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS의 경우도 똑같이 검사 결과를 Decode하는 절차가 필요하다. 하지만 애플의 경우 키 교환 등.... 직접 구현하기엔 너무 많은 보안절차가 필요하다.&lt;br /&gt;그렇다고 전용 엔드포인트를 제공하지도 않는다... 그래서 라이브러리를 사용하는 상황이 되었다;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증결과, 프론트엔드에서 생성된 KeyId, 그리고 Challenge code(Nonce)를 비교해 검증을 실시한다.&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 무결성 검사는 무척 중요한 파트이다. 하지만 국내에서는 잘 다뤄지지 않은 파트로 보인다.&lt;br /&gt;그래서 미래에 똑같은 일을 하게 될 상황을 대비해 기록을 남긴다.&lt;/p&gt;</description>
      <category>Java Kotlin/Spring SpringBoot</category>
      <category>Attest</category>
      <category>integrity</category>
      <category>ios appattest</category>
      <category>play integrity service</category>
      <category>SpringBoot</category>
      <category>무결성 검증</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/18</guid>
      <comments>https://molasses-0.tistory.com/18#entry18comment</comments>
      <pubDate>Mon, 12 May 2025 23:23:31 +0900</pubDate>
    </item>
    <item>
      <title>Springboot에서 Play Integrity API 사용하기 (2. Google 연동)</title>
      <link>https://molasses-0.tistory.com/17</link>
      <description>&lt;h1&gt;서론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.&lt;br /&gt;이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.&lt;/p&gt;
&lt;h1&gt;본론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글의 Integrity API Decode 과정에는 서비스 계정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://playintegrity.googleapis.com/v1/$packageName:decodeIntegrityToken&lt;/code&gt;에 Header로 &lt;code&gt;Authorization: Bearer&lt;/code&gt;를 넘겨줄 때, 본인의 계정을 사용할 필요가 없다. 오히려 사용해서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 서비스 계정을 생성, 그 서비스 계정의 json을 이용해 AccessToken을 받아오도록 하자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// implementation(&quot;com.google.auth:google-auth-library-oauth2-http:1.34.0&quot;)
@OptIn(DelicateCoroutinesApi::class)
@Scheduled(fixedRate = 3300000) // Refresh every 55 minutes
private fun refreshGoogleAccessToken() {
    GlobalScope.launch {
        try {
            val credentials = GoogleCredentials.fromStream(ByteArrayInputStream(googleAccountJson.toByteArray()))
                .createScoped(&quot;https://www.googleapis.com/auth/playintegrity&quot;)
            credentials.refresh()
            googleAccessToken = credentials.accessToken
        } catch (e: Exception) {
            throw RuntimeException(&quot;Failed to refresh Google access token: ${e.message}&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드로 AccessToken을 얻어올 수 있다. (googleAccountJson은 방금 받아온 서비스계정의 json 키 String이다.)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun verifyAndroidIntegrity(challenge: String, nonce: String): IntegrityVerificationResponse {
    return try {
        val decodedTokenBytes = webClient.post()
            .uri(decodeIntegrityTokenUrl)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(mapOf(&quot;integrity_token&quot; to challenge))
            .header(&quot;Authorization&quot;, &quot;Bearer ${googleAccessToken?.tokenValue}&quot;)
            .retrieve()
            .bodyToMono(ByteArray::class.java)
            .block()

        if (decodedTokenBytes == null) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;Decoded token is null&quot;
            )
        }

        val decodedTokenJson = String(decodedTokenBytes)

        val decodeResponse: DecodeIntegrityTokenResponse = gson.fromJson(decodedTokenJson, DecodeIntegrityTokenResponse::class.java)
        val tokenPayload = decodeResponse.tokenPayloadExternal

        // Package Name Verification
        if (tokenPayload.appIntegrity.packageName != packageName) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;Package name mismatch&quot;,
                details = mapOf(
                    &quot;expected&quot; to packageName,
                    &quot;actual&quot; to tokenPayload.requestDetails.requestPackageName
                )
            )
        }

        if (tokenPayload.requestDetails.nonce.replace(&quot;=&quot;, &quot;&quot;).trim() !=
            nonce.replace(&quot;=&quot;, &quot;&quot;).trim()
        ) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;Challenge mismatch&quot;,
                details = mapOf(
                    &quot;expected&quot; to nonce,
                    &quot;actual&quot; to tokenPayload.requestDetails.nonce
                )
            )
        }

        // Timestamp Verification
        if (isTokenExpired(tokenPayload.requestDetails.timestampMillis)) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;Token has expired&quot;,
                details = mapOf(&quot;timestamp&quot; to tokenPayload.requestDetails.timestampMillis)
            )
        }

        // App Integrity Verification
        if (tokenPayload.appIntegrity.appRecognitionVerdict != &quot;PLAY_RECOGNIZED&quot;) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;App not recognized by Google Play&quot;,
                details = mapOf(&quot;verdict&quot; to tokenPayload.appIntegrity.appRecognitionVerdict)
            )
        }

        // Device Integrity Verification
        if (!tokenPayload.deviceIntegrity.deviceRecognitionVerdict.contains(&quot;MEETS_DEVICE_INTEGRITY&quot;)) {
            return IntegrityVerificationResponse(
                isValid = false,
                message = &quot;Device integrity check failed&quot;,
                details = mapOf(&quot;verdicts&quot; to tokenPayload.deviceIntegrity.deviceRecognitionVerdict)
            )
        }

        IntegrityVerificationResponse(
            isValid = true,
            message = &quot;Android app integrity verification successful&quot;
        )

    } catch (e: Exception) {
        IntegrityVerificationResponse(
            isValid = false,
            message = &quot;Error verifying integrity: ${e.message}&quot;,
            details = mapOf(&quot;errorType&quot; to e.javaClass.simpleName)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드로 전체 검증을 실시한다. 앱이 설치된 환경(디바이스), 앱의 변조여부 등, 구글에서 범용적으로 검증 가능하다고 판단하는 기준을 따라 검증된 결과를 필터하는 코드이다.&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 한국에서도 잘 알려지지 않은 iOS AppAttest의 검증코드를 올릴 예정이다.&lt;/p&gt;</description>
      <category>Java Kotlin/Spring SpringBoot</category>
      <category>Attest</category>
      <category>integrity</category>
      <category>ios appattest</category>
      <category>play integrity service</category>
      <category>SpringBoot</category>
      <category>무결성 검증</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/17</guid>
      <comments>https://molasses-0.tistory.com/17#entry17comment</comments>
      <pubDate>Mon, 12 May 2025 23:18:13 +0900</pubDate>
    </item>
    <item>
      <title>Springboot에서 Play Integrity API 사용하기 (1. 기본 설정)</title>
      <link>https://molasses-0.tistory.com/16</link>
      <description>&lt;h1&gt;서론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업 프로젝트를 하다 React Native 환경의 클라이언트에서 앱 무결성 검증을 해야 하는 상황이 되었다.&lt;br /&gt;이 기록은 앱 무결성 검증을 하며 헤맨 부분의 기록이다.&lt;/p&gt;
&lt;h1&gt;본론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Play Integrity API는 앱의 무결성 검증. 즉 변조(해킹)된 앱으로 접속해 해킹하는 것을 방지하는 역할을 한다.&lt;br /&gt;무결성 검증은 배포 앱에서 가장 중요한 부분 중 하나이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선 Google Cloud Console의 설정을 요구한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 파트는 다른 블로그를 참고하길 바란다. 너무 잘 설명되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Firebase AppCheck와 직접 구현. 두 가지의 방법이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리는 직접 구현을 선택하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Springboot backend를 구현한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기가 가장 힘든 파트 중 하나이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 자신의 환경에 맞추어 @RestController 를 선언한다.&lt;br /&gt;나의 환경에 맞는 컨트롤러를 게재하겠다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/integrity&quot;)
class IntegrityController(
    @Autowired private val integrityService: IntegrityService,
    @Autowired private val challengeService: IntegrityChallengeService,
    private val integrityChallengeService: IntegrityChallengeService
) {
    private val logger = LoggerFactory.getLogger(this::class.java)

    @PostMapping(&quot;/challenge&quot;)
    fun getChallenge(
        @Valid @RequestBody request: ChallengeRequest
    ): ResponseEntity&amp;lt;ChallengeResponse&amp;gt; {
        try {
        val challenge = challengeService.generateChallenge(request.platform, request.deviceId)
            return ResponseEntity.ok(
                ChallengeResponse(
                    challenge = challenge.first ?: &quot;&quot;,
                    expiresInMinutes = challenge.second,
                    message = &quot;&quot;
                )
            )
        } catch(e: Exception) {
            logger.error(&quot;Error on getting challenge: ${e.message}&quot;)
            logger.debug(e.stackTraceToString())
            return ResponseEntity.ok(
                ChallengeResponse(
                    challenge = &quot;&quot;,
                    expiresInMinutes = 0,
                    message = &quot;An error occurred while generating challenge. Please try again later.&quot;
                )
            )
        }
    }

    @Transactional(timeout = 20)
    @PostMapping(&quot;/verify&quot;)
    fun verifyIntegrity(
        @RequestBody request: IntegrityVerificationRequest
    ): ResponseEntity&amp;lt;IntegrityVerificationResponse&amp;gt; {
        val challengeErrors = challengeService.verifyChallenge(request.challenge, request.deviceId)
        if(challengeErrors != null) return ResponseEntity.ok(challengeErrors)

        try {
            val result = when (request.platform.lowercase()) {
                &quot;android&quot; -&amp;gt; integrityService.verifyAndroidIntegrity(request.attestation, request.challenge)
                &quot;ios&quot; -&amp;gt; integrityService.verifyIosAppAttest(
                    request.attestation,
                    request.keyId ?: &quot;&quot;,
                    request.challenge
                )

                else -&amp;gt; IntegrityVerificationResponse(
                    isValid = false,
                    message = &quot;Unsupported platform: ${request.platform}&quot;
                )
            }

            integrityChallengeService.completeChallenge(request.challenge)
            return ResponseEntity.ok(result)
        } catch(e: Exception) {
            logger.error(&quot;Error on verifying integrity: ${e.message}&quot;)
            logger.debug(e.stackTraceToString())
            return ResponseEntity.ok(
                IntegrityVerificationResponse(
                    isValid = false,
                    message = &quot;An error occurred while verifying integrity. Please try again later.&quot;
                )
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Challenge Code (Nonce) 생성 파트와, 실제로 들어온 요청을 처리하는 엔드포인트 2개로 나뉘어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Challenge Code (Nonce) 관련 Service이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Service
class IntegrityChallengeService(
    @Autowired private val challengeRepository: IntegrityChallengeRepository,
    @Value(&quot;\${app.integrity-challenge-exp:15}&quot;) private val challengeExp: Int
) {
    private val logger = LoggerFactory.getLogger(this::class.java)
    private val secureRandom = SecureRandom()

    fun generateChallenge(deviceId: String): Pair&amp;lt;String?, Long&amp;gt; {
       val pendingChallenges = challengeRepository.findByDeviceIdAndStatus(
           deviceId,
           IntegrityChallengeStatus.PENDING
       )

        if(pendingChallenges.isNotEmpty()) {
            val latestChallenge = pendingChallenges.maxByOrNull { it.createdAt }

            if (
                latestChallenge != null &amp;amp;&amp;amp;
                latestChallenge.expiresAt.isAfter(LocalDateTime.now()) &amp;amp;&amp;amp;
                latestChallenge.status == IntegrityChallengeStatus.PENDING
            ) {
                logger.info(&quot;Challenge already exists for device: $deviceId&quot;)
                return Pair(latestChallenge.challenge, ChronoUnit.MINUTES.between(LocalDateTime.now(), latestChallenge.expiresAt))
            }
        }

        val bytes = ByteArray(32)
        secureRandom.nextBytes(bytes)

        val challenge = when(platform.lowercase()) {
            &quot;ios&quot; -&amp;gt; Base64.getEncoder().encodeToString(bytes)
            &quot;android&quot; -&amp;gt; Base64.getUrlEncoder().encodeToString(bytes)
            else -&amp;gt; throw RuntimeException(&quot;Unsupported platform: $platform&quot;)
        }
        val expiresAt = LocalDateTime.now().plusMinutes(challengeExp.toLong())

        val integrityChallenge = IntegrityChallenge(
            challenge = challenge,
            deviceId = deviceId,
            expiresAt = expiresAt,
        )

        challengeRepository.save(integrityChallenge)
        logger.info(&quot;Generated new integrity challenge for device: $deviceId, expiresAT: $expiresAt&quot;)
        return Pair(challenge, ChronoUnit.MINUTES.between(LocalDateTime.now(), expiresAt))
    }

    fun verifyChallenge(challenge: String, deviceId: String): IntegrityVerificationResponse? {
        val challengeOpt = challengeRepository.findByChallenge(challenge).getOrNull()

        if (challengeOpt == null) {
            logger.info(&quot;Challenge not found for device: $deviceId&quot;)
            return IntegrityVerificationResponse(false, &quot;Challenge not found&quot;)
        }

        if (challengeOpt.deviceId != deviceId) {
            logger.info(&quot;Device ID mismatch for challenge: $challenge, expected: $deviceId, actual: ${challengeOpt.deviceId}&quot;)
            return IntegrityVerificationResponse(false,&quot;Device ID mismatch&quot;)
        }

        val currentTime = LocalDateTime.now()
        challengeOpt.expiresAt?.let {
            if (it.isBefore(currentTime)) {
                challengeOpt.status = IntegrityChallengeStatus.EXPIRED
                challengeRepository.save(challengeOpt)
                logger.info(&quot;Challenge expired: $challenge&quot;)
                return IntegrityVerificationResponse(false,&quot;Challenge expired&quot;)
            }
        }

        return when (challengeOpt.status) {
            IntegrityChallengeStatus.COMPLETED -&amp;gt; IntegrityVerificationResponse(false, &quot;Challenge already used&quot;)
            IntegrityChallengeStatus.EXPIRED -&amp;gt; IntegrityVerificationResponse(false, &quot;Challenge expired&quot;)
            else -&amp;gt; null
        }
    }

    fun completeChallenge(challenge: String): Boolean {
        val challengeOpt = challengeRepository.findByChallenge(challenge).getOrNull()

        if(challengeOpt == null) {
            return false
        }

        challengeOpt.status = IntegrityChallengeStatus.COMPLETED
        challengeRepository.save(challengeOpt)
        logger.info(&quot;Challenge completed: $challenge&quot;)

        return true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nonce를 기록, 저장, 삭제하는 부분이다. 각 Nonce는 일정 시간(기본값: 15분)동안 Valid하고, 이후 Expire된다.&lt;br /&gt;이 Nonce는 Repository에 저장되고, Play Integrity Service, iOS AppAttest 모두 Nonce에 대한 검증을 같이 하게 된다.&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 기본적인 설정을 먼저 진행하였다.&lt;br /&gt;다음 글에서 Play Integrity Service 로직을 먼저 소개한 뒤, iOS AppAttest 로직을 소개할 예정이다.&lt;/p&gt;</description>
      <category>Java Kotlin/Spring SpringBoot</category>
      <category>Attest</category>
      <category>integrity</category>
      <category>ios appattest</category>
      <category>play integrity service</category>
      <category>SpringBoot</category>
      <category>무결성 검증</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/16</guid>
      <comments>https://molasses-0.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 12 May 2025 23:10:33 +0900</pubDate>
    </item>
    <item>
      <title>Turnstile은 뭔가요?</title>
      <link>https://molasses-0.tistory.com/15</link>
      <description>&lt;h3&gt;서론&lt;/h3&gt;
&lt;p&gt;많은 웹사이트에서 악성 사용자를 막기 위해 캡챠를 사용하곤 한다. 대표적으로 ReCaptcha가 있다.&lt;/p&gt;
&lt;p&gt;하지만 ReCaptcha는 일정 사용량 이상(10,000건)부터는 과금이 이루어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2668&quot; data-origin-height=&quot;1264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQFYDR/btsIPl2liVI/mooML6NLQktCIK4y8PAuV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQFYDR/btsIPl2liVI/mooML6NLQktCIK4y8PAuV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQFYDR/btsIPl2liVI/mooML6NLQktCIK4y8PAuV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQFYDR%2FbtsIPl2liVI%2FmooML6NLQktCIK4y8PAuV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2668&quot; height=&quot;1264&quot; data-origin-width=&quot;2668&quot; data-origin-height=&quot;1264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;하지만 많은 유용한 도구들은 대체제가 있기 마련이다. 내가 제시하는 대안은 Turnstile 이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image2.gif&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzuN0E/btsIRoiLdPP/eQJdfosHVZS5FNCX1M3hh0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzuN0E/btsIRoiLdPP/eQJdfosHVZS5FNCX1M3hh0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzuN0E/btsIRoiLdPP/eQJdfosHVZS5FNCX1M3hh0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bzuN0E/btsIRoiLdPP/eQJdfosHVZS5FNCX1M3hh0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;878&quot; height=&quot;300&quot; data-filename=&quot;image2.gif&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Turnstile의 장점은 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;빠르다&lt;/li&gt;
&lt;li&gt;정확하다.&lt;/li&gt;
&lt;li&gt;저렴하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cloudflare에서 완전 무료를 선언한 Turnstile은 빠르고 정확하며 저렴하기까지 하다.&lt;br&gt;ReCaptcha에서 넘어올 만 하지 않은가?&lt;/p&gt;
&lt;h3&gt;본론&lt;/h3&gt;
&lt;p&gt;사용법과 관련된 내용은 주로 &lt;a href=&quot;https://developers.cloudflare.com/turnstile/&quot;&gt;Turnstile Documentation&lt;/a&gt;에서 가져온 내용이다.&lt;/p&gt;
&lt;p&gt;먼저 client 검증이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script src=&amp;quot;https://challenges.cloudflare.com/turnstile/v0/api.js&amp;quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;div class=&amp;quot;cf-turnstile&amp;quot; data-sitekey=&amp;quot;yourSitekey&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;script 태그와 div 태그를 적절히 조합하면 위의 gif처럼 캡챠를 시작하는 모습을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;만약 의심스러운 트래픽이라고 판단한다면 바로 인증을 하라는 메세지를 볼 수도 있다.&lt;/p&gt;
&lt;p&gt;data-sitekey에는 당연히 Turnstile 앱을 만든 뒤 나오는 site key를 넣으면 된다.&lt;/p&gt;
&lt;p&gt;server에서는 이 turnstile에서 넘어온 데이터를 검증해야 한다. 이 검증과정도 간단하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &amp;#39;https://challenges.cloudflare.com/turnstile/v0/siteverify&amp;#39; --data &amp;#39;secret=verysecret&amp;amp;response=&amp;lt;RESPONSE&amp;gt;&amp;#39;`&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;우선 cURL 예문이다. Turnstile 앱을 만든 뒤 나오는 secret key와 캡챠 인증 후 나오는 response token을 전송하면&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{ success: Boolean }&lt;/code&gt; 형식의 응답이 온다. 더 정확히 응답형식을 보려면 Documentation을 확인하자!&lt;/p&gt;
&lt;p&gt;이걸 TypeScript와 fetch API로 바꾼다면 이렇게 변한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const response: { success: boolean } = await fetch(&amp;#39;https://challenges.cloudflare.com/turnstile/v0/siteverify&amp;#39;, {
  method: &amp;#39;POST&amp;#39;,
  body: JSON.stringify({
    secret: &amp;#39;verysecret&amp;#39;,
    response: &amp;#39;&amp;lt;RESPONSE&amp;gt;&amp;#39;
  })
})

if (response?.success ?? false) { /* here to on success */ }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ReCaptcha와 크게 다를것이 없다. &lt;a href=&quot;https://www.npmjs.com/package/vue-turnstile&quot;&gt;Vue 라이브러리&lt;/a&gt;와 &lt;a href=&quot;https://www.npmjs.com/package/@nuxtjs/turnstile&quot;&gt;Nuxt 라이브러리&lt;/a&gt;도 준비되어 있다. 찾아본다면 React, Svelte 등도 있을 것이다.&lt;/p&gt;
&lt;p&gt;(240926 수정)&lt;/p&gt;
&lt;p&gt;@nuxtjs/turnstile 의 &lt;code&gt;verifyTurnstileToken&lt;/code&gt; method를 사용할 때, Serverless 환경(ex: Cloudflare worker)에선 반드시 event를 두번째 인자로 넘겨줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;defineEventHandler(async (event: H3Event) =&amp;gt; {
      const token = &amp;quot; /* your token */ &amp;quot;
    const result = await verifyTurnstileToken(token, event)
})&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;Turnstile은 ReCaptcha와 견줄만 한 인증 시스템이다.&lt;/p&gt;
&lt;p&gt;요즈음 Cloudflare 사를 이용하는 사이트가 많아진 것도 Turnstile의 정확도에 기여하고 있을 것이다.&lt;/p&gt;
&lt;p&gt;ReCaptcha를 감당하지 못하는 소규모 프로젝트라면 Turnstile을 사용하는 것은 어떨까?&lt;/p&gt;</description>
      <category>TypeScript/Vuejs</category>
      <category>CloudFlare</category>
      <category>cloudflare turnstile</category>
      <category>recaptcha</category>
      <category>캡챠</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/15</guid>
      <comments>https://molasses-0.tistory.com/15#entry15comment</comments>
      <pubDate>Mon, 29 Jul 2024 14:27:02 +0900</pubDate>
    </item>
    <item>
      <title>Nuxthub 사용기 (Worker와 Nuxt를 같이 사용중이라면 필독)</title>
      <link>https://molasses-0.tistory.com/14</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare worker를 사용하는 &lt;a href=&quot;https://sh0rt.kr/&quot;&gt;sh0rt.kr&lt;/a&gt; 을 개발하며 가장 중요했던 것은 분산 Sqlite 호환 데이터베이스인 D1에 접근하는 방법이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D1에 접근하는 방법은 크게 두가지가 있다. Wrangler를 활용하는 방법과 &lt;a href=&quot;https://www.npmjs.com/package/@nuxthub/core&quot;&gt;@nuxthub/core&lt;/a&gt; 패키지를 사용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지의 차이점과 왜 @nuxthub/core 라이브러리를 사용해야 하는지를 간단히 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oyu7r/btsIPsletLn/ajyyBvj3gT6yy99x5Adyc0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oyu7r/btsIPsletLn/ajyyBvj3gT6yy99x5Adyc0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oyu7r/btsIPsletLn/ajyyBvj3gT6yy99x5Adyc0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyu7r%2FbtsIPsletLn%2FajyyBvj3gT6yy99x5Adyc0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Wrangler를 활용하려면 &lt;code&gt;wrangler dev&lt;/code&gt; 명령어를 통해 Worker 환경과 비슷한 로컬 환경을 만들 수 있다. 하지만 단점이 명확하다.&lt;br /&gt;Nuxt의 dev 모드에서는 D1에 접근할 수 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.cloudflare.com/ko-kr/developer-platform/d1/&quot;&gt;Cloudflare D1&lt;/a&gt;은 Sqlite와 호환되는 데이터베이스이다. 장점은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Worker와 같이 쓸 수 있는 DB다.&lt;/li&gt;
&lt;li&gt;Cloudflare의 각 엣지에 자동으로 배포되고, 데이터도 자동으로 증분된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Cloudflare worker를 도입하기 위해서 거의 무조건 활용하게 되는 데이터베이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점도 명확하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Cloudflare (Wrangler)에 종속적이다.&lt;/li&gt;
&lt;li&gt;트랜젝션 등 일부 기능은 활용할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 1번 문제가 개발과정에서 가장 크게 다가올 것이다.&lt;br /&gt;다만 이 문제는 해결법이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hub.nuxt.com/&quot;&gt;Nuxthub&lt;/a&gt;를 이용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nuxthub는 Cloudflare Worker를 리셀링하는 업체이자 Worker의 부속 서비스를 Nuxt에서 접근하기 쉽게 만들어주는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 나는 Cloudflare worker를 활용하고 있어 라이브러리를 활용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nuxthub의 &lt;a href=&quot;https://hub.nuxt.com/docs/getting-started/installation#add-to-a-nuxt-project&quot;&gt;Installation&lt;/a&gt; 문서를 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Nuxt3를 활용하는 프로젝트라면&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npx nuxi@latest module add hub
npm install -D wrangler&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어로 모듈 설치와 활성화를 해준 뒤,&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// nuxt.config.ts (.js)
  modules: ['@nuxthub/core'],
  hub: {
    // NuxtHub options
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션을 추가해주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hub에는 다음과 같은 기능들이 들어있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;database - D1과 연결&lt;/li&gt;
&lt;li&gt;blob - R2와 연결&lt;/li&gt;
&lt;li&gt;analytics - &lt;a href=&quot;https://developers.cloudflare.com/analytics/analytics-engine/&quot;&gt;Analytics engine(beta)&lt;/a&gt;와 연결&lt;/li&gt;
&lt;li&gt;kv - Key-value 스토리지인 KV와 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 다양한 기능이 숨어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 중 database만 활용하고, Analytics Engine은 Production 환경에서만 작동하도록 코드를 구성했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nuxt3와 Cloudflare worker를 같이 활용중이라면 엄청 매력적인 라이브러리이다. 만약 Worker를 직접 배포하기 싫다면 &lt;a href=&quot;https://hub.nuxt.com&quot;&gt;Nuxthub&lt;/a&gt; 리셀링을 활용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 Turnstile에 대해 써보도록 하겠다. 이것도 Nuxt 환경에서 캡챠 띄우기와 검증이 금방 이루어진다.&lt;/p&gt;</description>
      <category>TypeScript/Vuejs</category>
      <category>cloudflare d1</category>
      <category>cloudflare worker</category>
      <category>nuxthub</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/14</guid>
      <comments>https://molasses-0.tistory.com/14#entry14comment</comments>
      <pubDate>Fri, 26 Jul 2024 14:42:57 +0900</pubDate>
    </item>
    <item>
      <title>Nuxtjs3(unjs Nitro.js) Cloudflare D1 개발기</title>
      <link>https://molasses-0.tistory.com/13</link>
      <description>&lt;h3&gt;서론&lt;/h3&gt;
&lt;p&gt;Cloudflare worker를 이용해 URL Shorter를 만들어보려는 사이드 프로젝트 계획을 시작했다.&lt;/p&gt;
&lt;p&gt;하지만 wrangler를 이용해 디버깅을 해야하는 등, 만만치 않았던 과정이였다.&lt;/p&gt;
&lt;p&gt;하지만 Nitro.js의 문서를 살펴보다 보니 새로운 방법을 찾게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nSEvb/btsHBeJRXnP/FunBleQJkAmOM6q9HF71d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nSEvb/btsHBeJRXnP/FunBleQJkAmOM6q9HF71d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nSEvb/btsHBeJRXnP/FunBleQJkAmOM6q9HF71d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnSEvb%2FbtsHBeJRXnP%2FFunBleQJkAmOM6q9HF71d0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;본론&lt;/h3&gt;
&lt;p&gt;살펴본 &lt;a href=&quot;https://nitro.unjs.io/deploy/providers/cloudflare#cloudflare-service-workers&quot;&gt;문서는 다음과 같다.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cloudflare는 Worker와 D1, KV Namespace와 같은 Serverless 환경을 제공하고 있다.&lt;br&gt;하지만 이것을 이용해 개발하는 과정은 조금 험난하다.&lt;/p&gt;
&lt;p&gt;wrangler라는 cli 도구를 이용해야 개발 환경에서 D1과 같은 서비스에 접근하는 코드를 짤 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 이제는 다르다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/nitro-cloudflare-dev&quot;&gt;nitro-cloudflare-dev&lt;/a&gt; 패키지를 설치해 보자.&lt;/p&gt;
&lt;p&gt;Nuxt3에서는 설치 후 다음과 같이 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineNuxtConfig({
  modules: [&amp;quot;nitro-cloudflare-dev&amp;quot;],
})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;자세히는 해당 패키지의 npm 페이지를 살펴보자.&lt;/p&gt;
&lt;p&gt;이 패키지를 사용하면 로컬 개발환경(&lt;code&gt;npm run dev&lt;/code&gt;) 에서도 &lt;code&gt;event.context.cloudflare&lt;/code&gt; 에 접근이 가능하다!&lt;/p&gt;
&lt;p&gt;D1 binding namespace가 &lt;code&gt;DB&lt;/code&gt;로 되어있다면 다음과 같은 코드를 사용할 수 있다!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {H3Event} from &amp;quot;h3&amp;quot;;

export default function (event: H3Event) {
    const { cloudflare } = event.context
    return cloudflare.env.DB
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;비슷한 개발을 하는 분이 생길 것 같다.&lt;/p&gt;
&lt;p&gt;앞으로 서버리스가 대세가 되어가고 있기 때문이다.&lt;/p&gt;
&lt;p&gt;이 글을 보면서 헤메지 않았으면 한다.&lt;/p&gt;</description>
      <category>TypeScript/Vuejs</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/13</guid>
      <comments>https://molasses-0.tistory.com/13#entry13comment</comments>
      <pubDate>Fri, 24 May 2024 21:46:56 +0900</pubDate>
    </item>
    <item>
      <title>async/await 문법?? 먹는건가요?</title>
      <link>https://molasses-0.tistory.com/12</link>
      <description>&lt;h3&gt;서론&lt;/h3&gt;
&lt;p&gt;지난번에는 Callback hell을 해결하기 위해 &lt;a href=&quot;https://blog.mori.space/11&quot;&gt;Promise 문법&lt;/a&gt;을 알아보았다.&lt;/p&gt;
&lt;p&gt;하지만 Callback hell을 완벽하게 회피할 수 있는 방법은 아니다.&lt;br&gt;그래서 async/await 문법이 ES7부터 나오게 되었다.&lt;/p&gt;
&lt;p&gt;ES7의 async/await에 대해 알아보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vvGnv/btsHxJJbvHN/0ef2tUT6zQtAZKK1iHmAVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vvGnv/btsHxJJbvHN/0ef2tUT6zQtAZKK1iHmAVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vvGnv/btsHxJJbvHN/0ef2tUT6zQtAZKK1iHmAVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvvGnv%2FbtsHxJJbvHN%2F0ef2tUT6zQtAZKK1iHmAVK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;본문&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import fs from &amp;#39;fs&amp;#39;

const openFile = (file: string) =&amp;gt; new Promise((resolve, reject) =&amp;gt; {
  fs.readFile(file, (err, data) =&amp;gt; {
    if(err) reject(err);
    resolve(data);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지난번 포스트의 openFile 함수다. 이걸 쓰기 위해&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;openFile(&amp;#39;location&amp;#39;)
  .catch((err) =&amp;gt; console.log(err))
  .then((data) =&amp;gt; console.log(data));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.then .catch 문법을 사용했다. 하지만 이문법도 .then 이나 .catch 의 callback 함수에서 Promise 를 사용하는 패턴이 생긴다면 Callback hell을 완전히 막을 없다.&lt;/p&gt;
&lt;p&gt;그래서 나온 것이 async/await 문법이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;try {
  const data = await openFile(&amp;#39;location&amp;#39;);
  console.log(data)
} catch(err) {
  console.log(err)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;callback hell이 완전히 없어졌다!&lt;/p&gt;
&lt;p&gt;하지만 이 문법은 ES7 이하에선 babel을 이용해 Promise로 번역하는 작업이 필요하다.&lt;/p&gt;
&lt;p&gt;그렇다면 Promsie.all([])과 같은 문법은 어떻게 사용하는지 물어볼 수 있다. 간단하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;try {
  const data = await Promise.all([openFile(&amp;#39;file1&amp;#39;), openFile(&amp;#39;file2&amp;#39;)];
  console.log(data)
} catch(err) {
  console.log(err)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 사용한다면 data에는 openFile(&amp;#39;file1&amp;#39;)과 openFile(&amp;#39;file2&amp;#39;)의 결과가 Array에 담겨서 반환될 것이다.&lt;/p&gt;
&lt;p&gt;Error 처리는 단순히 Try/catch 문을 사용하면 된다!&lt;/p&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;최근 TypeScript와 JavaScript에서는 callback hell을 피하기 위한 노력이 결실을 맺었다.&lt;br&gt;하지만 구버전에서 Native로 지원되지 않기 때문에 TypeScript 설정을 조정하거나 Babel을 사용할 필요가 있다.&lt;/p&gt;
&lt;p&gt;하지만 상당히 멋진 문법이 만들어졌다는 것에 존경을 표한다.&lt;/p&gt;</description>
      <category>TypeScript</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/12</guid>
      <comments>https://molasses-0.tistory.com/12#entry12comment</comments>
      <pubDate>Wed, 22 May 2024 15:12:21 +0900</pubDate>
    </item>
    <item>
      <title>Promise는 어떤 건가요?</title>
      <link>https://molasses-0.tistory.com/11</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise는 JavaScript의 비동기 프로그래밍을 도와주는 라이브러리다.&lt;br /&gt;현재는 JavaScript V8엔진에 기본 탑재되어 있다. (즉 Node.js에서도 활용할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IE10 이전 버전에서는 작동하지 않는데 Babel과 Promise polyfill을 통해 비슷하게 작동하도록 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;File_JavaScript-logo.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwo3bh/btsHw4tGSwD/KN17FNJmWgYUWYeQ16pl40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwo3bh/btsHw4tGSwD/KN17FNJmWgYUWYeQ16pl40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwo3bh/btsHw4tGSwD/KN17FNJmWgYUWYeQ16pl40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwo3bh%2FbtsHw4tGSwD%2FKN17FNJmWgYUWYeQ16pl40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1052&quot; height=&quot;1052&quot; data-filename=&quot;File_JavaScript-logo.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;1052&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 Promise가 비동기 작동을 돕는걸까?&lt;br /&gt;간단한 Wait 함수를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;const wait = (time: number): Promise&amp;lt;void&amp;gt; =&amp;gt; new Promise((resolve, reject) =&amp;gt; {
  setTimeout(() =&amp;gt; resolve(), time);
});

wait(4000).then(() =&amp;gt; console.log(&quot;done&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 실행한다면 4초 후 Promise 객체의 resolve 함수가 실행되며 .then() 절로 넘어가 &lt;code&gt;done&lt;/code&gt; 가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise 객체의 resolve 함수는 함수가 정상적으로 실행되었음을 의미한다. 여기에 변수를 담을 수 있다.&lt;br /&gt;reject 함수는 에러가 발생했음을 의미한다. 마찬가지로 변수를 담을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import fs from 'fs'

const openFile = (file: string) =&amp;gt; new Promise((resolve, reject) =&amp;gt; {
  fs.readFile(file, (err, data) =&amp;gt; {
    if(err) reject(err);
    resolve(data);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;openFile('location')
  .catch((err) =&amp;gt; console.log(err))
  .then((data) =&amp;gt; console.log(data));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise는 callback hell을 조금이나마 완화할 수 있는 문법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 callback hell을 유발할 가능성이 남아있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 게시글에서 이를 완전히 해결한 async/await 문법에 대해 알아보겠다.&lt;/p&gt;</description>
      <category>TypeScript</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/11</guid>
      <comments>https://molasses-0.tistory.com/11#entry11comment</comments>
      <pubDate>Wed, 22 May 2024 14:31:45 +0900</pubDate>
    </item>
    <item>
      <title>oh-my-zsh powerlevel10k 설치하기</title>
      <link>https://molasses-0.tistory.com/10</link>
      <description>&lt;h3&gt;서론&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot;&gt;Powerlevel10k&lt;/a&gt;도 oh-my-zsh에 엮어 많이 사용하는 라이브러리다.&lt;/p&gt;
&lt;p&gt;간단히 설치하고 사용할 수 있고, 비주얼 적으로도 좋아 자주 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm0VR4/btsHpzlwfpG/IyCzkb4juYJ7KZH2Ilffgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm0VR4/btsHpzlwfpG/IyCzkb4juYJ7KZH2Ilffgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm0VR4/btsHpzlwfpG/IyCzkb4juYJ7KZH2Ilffgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm0VR4%2FbtsHpzlwfpG%2FIyCzkb4juYJ7KZH2Ilffgk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;본론&lt;/h3&gt;
&lt;p&gt;우선 &lt;a href=&quot;https://molasses-0.tistory.com/9&quot;&gt;여기&lt;/a&gt;를 참고해 oh-my-zsh와 폰트를 설치해준다.&lt;/p&gt;
&lt;p&gt;두번째로 &lt;a href=&quot;https://github.com/romkatv/powerlevel10k?tab=readme-ov-file#oh-my-zsh&quot;&gt;여기&lt;/a&gt;를 참고해 설치할 수 있다.&lt;/p&gt;
&lt;p&gt;oh-my-zsh와 연계해 설치하는 코드는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;마지막으로 &lt;code&gt;~/.zshrc&lt;/code&gt;의 theme 부분을 다음과 같이 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Set name of the theme to load --- if set to &amp;quot;random&amp;quot;, it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME=&amp;quot;powerlevel10k/powerlevel10k&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;설정한 뒤 &lt;code&gt;source ~/.zshrc&lt;/code&gt; 를 실행하면 powerlevel10k의 최초 설정과정이 알아서 진행된다.&lt;/p&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;oh-my-zsh와 powerlevel10k를 이용해 편리하고 좋은 개발환경을 느끼길 바란다.&lt;/p&gt;</description>
      <category>기타 기술스택</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/10</guid>
      <comments>https://molasses-0.tistory.com/10#entry10comment</comments>
      <pubDate>Mon, 13 May 2024 20:37:46 +0900</pubDate>
    </item>
    <item>
      <title>oh-my-zsh &amp;amp; autosuggestion, syntax-highlighting 설치하기</title>
      <link>https://molasses-0.tistory.com/9</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람이 zsh의 매력을 느끼고 zsh, ohmyzsh를 사용하시는 경우가 많다.&lt;br /&gt;하지만 zsh를 처음 쓴다면 의외로 단조로운 쉘 때문에 당황할 지도 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 쓰면 좋을 zsh 확장인 oh-my-zsh와 zsh-autosuggestion, zsh-syntax-highlighting 설치방법에 대해 이야기한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TcnBo/btsHofhg66Q/JAK3DC5XSEJQmoJcr4T180/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TcnBo/btsHofhg66Q/JAK3DC5XSEJQmoJcr4T180/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TcnBo/btsHofhg66Q/JAK3DC5XSEJQmoJcr4T180/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTcnBo%2FbtsHofhg66Q%2FJAK3DC5XSEJQmoJcr4T180%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;337&quot; height=&quot;208&quot; data-origin-width=&quot;337&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;a href=&quot;https://ohmyz.sh&quot;&gt;oh-my-zsh&lt;/a&gt;의 설치코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 cURL은 우분투나 맥에 다 깔려있기 때문에 cURL을 사용하는 방식을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투 환경에서 zsh가 깔려있지 않다면 다음 명령어를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo apt install zsh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oh-my-zsh 기본설정 과정에 zsh를 기본 쉘로 설정하는 옵션도 있으니 참고 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째로 두개의 확장을 설치해야 한다. 참고한 곳은 &lt;a href=&quot;https://gist.github.com/n1snt/454b879b8f0b7995740ae04c5fb5b7df&quot;&gt;다음과 같다&lt;/a&gt;&lt;br /&gt;이 확장 플러그인은 여러 장소에 설명되어 있으나, 설명하는 장소마다 조금씩 다르게 설명하고 있다.&lt;br /&gt;필자는 실제로 설치하여 플러그인 2개만 설치하면 되는 것을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;git clone https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 &lt;code&gt;zsh-autosuggestions&lt;/code&gt;, &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt; 플러그인만 git clone 명령어를 이용해 받아주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번째로 &lt;code&gt;~/.zshrc&lt;/code&gt; 파일을 조금 수정해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;# Set name of the theme to load --- if set to &quot;random&quot;, it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME=&quot;agnoster&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 agnoster 테마 혹은 &lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot;&gt;powerlevel10k&lt;/a&gt;를 추천한다. 하지만 두 테마 모두 별도 폰트가 필요하다. 이것은 마지막 내용으로 설명하겠다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git zsh-autosuggestions zsh-syntax-highlighting)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치한 플러그인인 &lt;code&gt;zsh-autosuggestions&lt;/code&gt;, &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt; 을 넣으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 다음과 같은 플러그인들을 사용중이다. 별도 설치한 두 개의 플러그인을 제외하면 모두 oh-my-zsh에서 기본으로 제공되는 플러그인이다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;plugins=(git docker git-flow git-lfs gpg-agent node npm brew zsh-autosuggestions zsh-syntax-highlighting)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 다음을 설정해준다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;setopt HIST_EXPIRE_DUPS_FIRST
setopt HIST_IGNORE_DUPS
setopt HIST_IGNORE_ALL_DUPS
setopt HIST_IGNORE_SPACE
setopt HIST_FIND_NO_DUPS
setopt HIST_SAVE_NO_DUPS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 setopt문의 역할은 commandline history의 중복을 방지해주는 옵션들이다. 같은 명령어를 두번 이상 사용해도 history에는 한번만 나오도록 설정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 폰트 설정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-13 19.34.57.png&quot; data-origin-width=&quot;2110&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvVIQu/btsHnhGXJRc/KicTIa1uEzjKisZaYu2nfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvVIQu/btsHnhGXJRc/KicTIa1uEzjKisZaYu2nfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvVIQu/btsHnhGXJRc/KicTIa1uEzjKisZaYu2nfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvVIQu%2FbtsHnhGXJRc%2FKicTIa1uEzjKisZaYu2nfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2110&quot; height=&quot;1186&quot; data-filename=&quot;스크린샷 2024-05-13 19.34.57.png&quot; data-origin-width=&quot;2110&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Iterm2 설정에서 Profiles, Text 순으로 들어가면 Font 설정이 있다. 여기에서 Ligature 옵션이 있는 폰트들을 사용하면 된다.&lt;br /&gt;예를 들어 &lt;a href=&quot;https://github.com/naver/d2codingfont&quot;&gt;Naver의 D2coding 폰트&lt;/a&gt;나 powerlevel10k에서도 사용할 수 있는 &lt;a href=&quot;https://github.com/andreberg/Meslo-Font&quot;&gt;MesloGS&lt;/a&gt; 를 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투나 맥의 기본 터미널에서도 비슷한 옵션을 제공하고 있다. 잘 확인해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 ligatures를 지원하는 폰트를 자유자재로 사용할 수 있다. 아래의 Use ligatures 옵션은 반드시 켜야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zsh를 사용하면서 oh-my-zsh나 다른 좋은 플러그인들을 활용하지 못해 개발이나 다른 작업이 효율적이지 못하거나, 예전에 사용한 명령어를 잊어버리는 경우가 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔은 너무 많은 history가 쌓여 용량을 차지하거나 이전에 사용한 명령어를 찾기 곤란한 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴때 이 설정들로 어려움을 타파해보길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[powerlevel10k 설정 방법](&lt;a href=&quot;https://molasses-0.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://molasses-0.tistory.com/10&lt;/a&gt;)도 설명하고 있으니 참고 바란다!&lt;/p&gt;</description>
      <category>기타 기술스택</category>
      <category>oh-my-zsh</category>
      <category>zsh</category>
      <category>zsh-autosuggestions</category>
      <category>zsh-syntax-highlighting</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/9</guid>
      <comments>https://molasses-0.tistory.com/9#entry9comment</comments>
      <pubDate>Mon, 13 May 2024 19:42:15 +0900</pubDate>
    </item>
    <item>
      <title>Python Pydantic에 대해서</title>
      <link>https://molasses-0.tistory.com/8</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI를 사용하다보면 Pydantic을 이용해 커스텀 class를 정의해야 할 일이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Pydantic의 BaseModel class를 상속한 CorrectionRequest class를 정의하는 모습이다.&lt;br /&gt;실제 &lt;a href=&quot;https://correction.mori.space/&quot;&gt;맞춤법 검사기&lt;/a&gt;에서 사용하는 클래스이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class CorrectionRequest(BaseModel):
    text: str
    correction: str
    memo: str
    recaptcha_response: str&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Pydantic은 무엇이고 왜 사용하는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6d31d0d9-6770-4cbc-90d5-a611662126ee.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BnMkz/btsHngmH4RX/RN0RFCGk4IoJmw1alEicG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BnMkz/btsHngmH4RX/RN0RFCGk4IoJmw1alEicG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BnMkz/btsHngmH4RX/RN0RFCGk4IoJmw1alEicG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBnMkz%2FbtsHngmH4RX%2FRN0RFCGk4IoJmw1alEicG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-filename=&quot;6d31d0d9-6770-4cbc-90d5-a611662126ee.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.pydantic.dev/latest/&quot;&gt;Pydantic&lt;/a&gt;의 공식 Docs의 서문을 인용하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Pydantic is the most widely used data validation library for Python.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pydantic은 데이터 검증 라이브러리이다.&lt;br /&gt;즉 데이터를 담은 객체에 정한대로 데이터가 잘 정의되어 있는지 검사하는 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaseModel을 상속한 class를 이용해 object를 하나 정의한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;request = CorrectionRequest(
    text='abcd',
    correction='abdc',
    memo='test',
    recaptcha_response='token'
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 데이터의 Type이나 Missing(누락) 등을 검사해주는 역할, JSON scheme로 변환해주는 역할 등을 담당하는 것이 Pydantic이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;try:
    request = CorrectionRequest(
        text='abcd',
        correction='abdc',
    )
except ValidationError as e:
    println(e)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드에서는 ValidationError로 memo field와 recaptcha_response field가 누락되었다는 메세지가 뜬다.&lt;br /&gt;타입에러도 ValidationError가 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI를 사용하다 보면 Pydantic을 많이 사용하게 된다.&lt;br /&gt;Serializer를 정의해 DB 쿼리까지 한 큐에 진행되는 Django Rest Framework와 다르게 Pydantic과 FastAPI는 데이터를 DB에 쿼리하는 부분은 직접 코드를 작성해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Django Rest Framework와 FastAPI를 모두 사용해 본 입장으로 자유도가 높은 FastAPI가 조금 더 개발하기 편하다는 느낌이 들었다.&lt;/p&gt;</description>
      <category>Python</category>
      <category>pydantic</category>
      <category>Python</category>
      <category>python pydantic</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/8</guid>
      <comments>https://molasses-0.tistory.com/8#entry8comment</comments>
      <pubDate>Mon, 13 May 2024 10:50:26 +0900</pubDate>
    </item>
    <item>
      <title>Oracle M1 MAC에서 사용하기 (23c free)</title>
      <link>https://molasses-0.tistory.com/7</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학 수업을 듣던 도중, 데이터베이스 과목에서 Oracle을 사용하게 되었다.&lt;br /&gt;하지만 M3 맥북프로를 사용하는 나는 Oracle를 Native로 돌릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 사용하여 최신의 Oracle 23x free를 받아 사용하는 방법을 옮겨적고자 한다.&lt;br /&gt;참고로 모 학교의 에브리타임에 있는 내용과 동일한 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Oracle-Logo-1.png&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTDLj/btsHdwRY68J/IOEkU95Q21l1ePFjf0F0GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTDLj/btsHdwRY68J/IOEkU95Q21l1ePFjf0F0GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTDLj/btsHdwRY68J/IOEkU95Q21l1ePFjf0F0GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTDLj%2FbtsHdwRY68J%2FIOEkU95Q21l1ePFjf0F0GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3840&quot; height=&quot;2160&quot; data-filename=&quot;Oracle-Logo-1.png&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 준비물. &lt;a href=&quot;https://whalec.io/homebrew-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95/&quot;&gt;Homebrew&lt;/a&gt; 여기를 참고해 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 &lt;a href=&quot;https://formulae.brew.sh/formula/colima&quot;&gt;Colima&lt;/a&gt; 라는 경량 Docker 런타임(버추얼머신 비슷한 녀석)을 깔아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;brew install colima&lt;/code&gt; 명령어를 사용하면 간단히 깔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;a href=&quot;https://formulae.brew.sh/formula/docker&quot;&gt;Docker client&lt;/a&gt; 를 설치하는데, 이건 간단하게 &lt;code&gt;brew install docker&lt;/code&gt; 로 깔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 Docker를 실행할 x86 기반의 버추얼머신을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;colima start --network-address --memory 4 --arch x86_64&lt;/code&gt; 명령어로 간단히 4GB짜리 x86_64 버추얼머신을 띄워준다.&lt;br /&gt;명령어를 해석하자면 network address 기능을 사용하고, 램 4GB, x86_64(amd64)의 버추얼머신을 가동하라는 명령어다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9zcBy/btsHcnN9FTM/FkLXcJpFaeB9OMofRChBw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9zcBy/btsHcnN9FTM/FkLXcJpFaeB9OMofRChBw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9zcBy/btsHcnN9FTM/FkLXcJpFaeB9OMofRChBw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9zcBy%2FbtsHcnN9FTM%2FFkLXcJpFaeB9OMofRChBw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2956&quot; height=&quot;1754&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 모든 블로그가 동일하다.&lt;br /&gt;다른 블로그를 봤다면 &lt;code&gt;Oracle x21 XE&lt;/code&gt;가 깔려 있을 것이다. 우리는 이 이미지가 더이상 필요하지 않으니 지울 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker container ls -al 로 컨테이너의 이름을 알아낸다. 컨테이너의 이름은 NAMES 레이블에 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhWZt/btsHedjRekL/LflLaNeWaZgpCAlBGGUt71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhWZt/btsHedjRekL/LflLaNeWaZgpCAlBGGUt71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhWZt/btsHedjRekL/LflLaNeWaZgpCAlBGGUt71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhWZt%2FbtsHedjRekL%2FLflLaNeWaZgpCAlBGGUt71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2956&quot; height=&quot;1754&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 알아낸 이름(이미지는 oracle 일 때)을 통해 삭제하는 법은 &lt;code&gt;docker container rm oracle --force&lt;/code&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 드디어 Oracle 설치의 시간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker run -d -p 1521:1521 -e &quot;ORACLE_PASSWORD=oraclepass&quot; -v oracle-volume:/opt/oracle/oradata gvenzl/oracle-free&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어로 Oracle Container를 만들어준다.&lt;br /&gt;컨테이너 이름을 알고싶다면 docker container ls -al 을 쳐주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 이미지와 비슷하지만 컨테이너 이름이 랜덤으로 부여되어 있다. 또 restart-policy를 제대로 설정하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker rename (컨테이너 이름) oracle&lt;/code&gt; 로 컨테이너 이름을 oracle로 바꿔준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 restart-policy를 업데이트 한다. &lt;code&gt;docker update --restart always (컨테이너 이름)&lt;/code&gt; 명령어로 간단히 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pdruV/btsHgidzZ4v/KLVt9KGUCvLszfaCct8MyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pdruV/btsHgidzZ4v/KLVt9KGUCvLszfaCct8MyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pdruV/btsHgidzZ4v/KLVt9KGUCvLszfaCct8MyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpdruV%2FbtsHgidzZ4v%2FKLVt9KGUCvLszfaCct8MyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2956&quot; height=&quot;1754&quot; data-origin-width=&quot;2956&quot; data-origin-height=&quot;1754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 패스워드가 이상하다면 다음 명령어를 사용한다. oraclepass를 원하는 패스워드로 바꾸면 된다. 아쉽게도 이 명령어는 민감한 정보이다 보니 사진으로 담지는 못했다. 다만 이 명령어는 &lt;code&gt;gvenzl/oracle-free&lt;/code&gt; 이미지에는 공통적으로 사용할 수 있는 명령어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docker exec (컨테이너 이름) resetPassword &quot;oraclepass&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 접속 파트다. 나는 DataGrip를 사용한다. JetBrains 학생인증을 받은 상태여서 무료로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이나 PostgreSQL을 사용해본 적이 있다면 접속정보가 조금 다른것을 바로 눈치챌 수 있다.&lt;br /&gt;Oracle은 MySQL과 다르게 sid라는 정보로 추가인증을 하게 된다. 하지만 &lt;code&gt;Oracle 21c XE&lt;/code&gt;와 다르게 이 Docker image는 SID가 free로 설정되어 있다. 따라서 다음 이미지와 같이 설정해줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQwHNN/btsHfYNdQ9e/9IZzYpcqkP0oSK0ZHmVNB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQwHNN/btsHfYNdQ9e/9IZzYpcqkP0oSK0ZHmVNB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQwHNN/btsHfYNdQ9e/9IZzYpcqkP0oSK0ZHmVNB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQwHNN%2FbtsHfYNdQ9e%2F9IZzYpcqkP0oSK0ZHmVNB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1598&quot; height=&quot;1364&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;1364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 colima stop과 start만 알려준다면 이 글은 완전히 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;colima는 하이퍼바이저기 때문에 배터리 사용량이 어마어마하다. 쓸모없는 하드웨어 자원을 먹기도 한다. 따라서 필요할 때만 켜서 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;colima stop&lt;/code&gt; 명령어로 간단히 종료, &lt;code&gt;colima start&lt;/code&gt; 명령어로 다시 시작할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지의 블로그는 &lt;code&gt;Oracle 21c XE&lt;/code&gt; 버전을 기준으로 설명되어 있다. 하지만 Maintainer가 이 Image를 더 이상 유지보수하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 글을 보고 최신버전을 사용하기를 권장하는 바이다.&lt;/p&gt;</description>
      <category>기타 기술스택/Docker</category>
      <category>colima</category>
      <category>M1 Mac</category>
      <category>Oracle</category>
      <category>oracle m1 mac</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/7</guid>
      <comments>https://molasses-0.tistory.com/7#entry7comment</comments>
      <pubDate>Wed, 8 May 2024 11:16:29 +0900</pubDate>
    </item>
    <item>
      <title>DjangoRestFramework SlugRelatedField get_or_create</title>
      <link>https://molasses-0.tistory.com/6</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DjangoRestFramework(이하 DRF)로 사이트를 구현하다 보면 해시태그와 같이 SlugRelatedField에서 get_or_create 연산을 해야 하는 경우가 생긴다.&lt;br /&gt;하지만 DRF의 SlugRelatedField에서는 이걸 지원하지 않는다.&lt;br /&gt;우리에게는 class 상속이 있다! 상속을 통해 이 문제를 해결해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;logo.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnztZS/btsHfDv9c1k/cbaJC4rDUs8JXKmPoKLYvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnztZS/btsHfDv9c1k/cbaJC4rDUs8JXKmPoKLYvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnztZS/btsHfDv9c1k/cbaJC4rDUs8JXKmPoKLYvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnztZS%2FbtsHfDv9c1k%2FcbaJC4rDUs8JXKmPoKLYvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;265&quot; data-filename=&quot;logo.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성한 코드는 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.StringRelatedField(source='owner.profile.nickname', read_only=True)
    answer = AnswerSerializer(many=True, source='answer_set', read_only=True)
    topic = SlugRelatedGetOrCreateField(
        slug_field='content', many=True, queryset=QuestionTopic.objects.all()
    )

    class Meta:
        model = Question
        fields = ('id', 'subject', 'content', 'created_at', 'updated_at',
                  'topic', 'owner', 'owner_id', 'answer', 'hit')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic 라는 필드는 &lt;code&gt;List[str]&lt;/code&gt; 으로 구성된다. 하지만 QuestionSerializer.save() method를 실행할 때 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 &lt;code&gt;SlugRelatedField&lt;/code&gt;는 topics = QuestionTopic.objects.get() 을 수행한 뒤 topics 안에 topic의 item이 없다면 Error를 띄운다.&lt;br /&gt;하지만 우리가 원하는 것은 item이 없다면 만드는 것이다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 통해 찾은 해법은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/a/69330557/11516704&quot;&gt;StackOverflow&lt;/a&gt; 에선 &lt;code&gt;SlugRelatedField&lt;/code&gt; 를 상속한 &lt;code&gt;SlugRelatedGetOrCreateField&lt;/code&gt; class를 만들고, 이걸 적용한다는 해법을 제시한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class SlugRelatedGetOrCreateField(serializers.SlugRelatedField):
    def to_internal_value(self, data):
        queryset = self.get_queryset()
        try:
            return queryset.get_or_create(**{self.slug_field: data})[0]
        except (TypeError, ValueError):
            self.fail(&quot;invalid&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마법처럼 문제가 해결되었다...&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DRF도 많은 사람이 사용하다보니 문제에 대해 많은 해법이 제시되어 있었다.&lt;br /&gt;하지만 이 해법 중 나에게 맞는 해법을 찾는건 노력이 조금 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 한글로 된 문서는 적었기에 내가 해결한 과정을 남겨 다른 사람들이 조금 더 보기 쉽도록 만들고 싶었다.&lt;br /&gt;여러분도 이 글을 보며 도움이 되길 바란다.&lt;/p&gt;</description>
      <category>Python/Django</category>
      <category>djangorestframework</category>
      <category>drf</category>
      <category>slugrelatedfield</category>
      <category>slugrelatedgetorcreatefield</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/6</guid>
      <comments>https://molasses-0.tistory.com/6#entry6comment</comments>
      <pubDate>Wed, 8 May 2024 10:41:06 +0900</pubDate>
    </item>
    <item>
      <title>Nuxtjs3 RuntimeConfig Naming Convention</title>
      <link>https://molasses-0.tistory.com/5</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nuxtjs를 쓰면서 RuntimeConfig를 사용할 일이 많이 있다. 예를 들자면 Adsense의 광고ID나 Backend URL 등 의외로 많은 장소에서 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RuntimeConfig의 내용은 ServerSide에서도, ClientSide에서도 모두 사용이 가능한 변수이다. process.env.[]는 ServerSide에서만 사용이 가능한 것과 대조된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정방법을 모르는 분을 위해 설정 예시를 보이고 본론으로 들어가도록 하겠다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      backendUrl: process.env.NUXT_BACKEND_URL ?? &quot;http://localhost:8000&quot;,
      recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY
    }
  },
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 &lt;a href=&quot;https://correction.mori.space/&quot;&gt;맞춤법 검사기&lt;/a&gt;의 코드 일부를 가져온 것이다. 여기에서 사용중인 BackendURL과 ReCaptcha의 SITE_KEY를 runtimeConfig의 public variable로 넣은 모습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;logo-green-black.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWsYLn/btsHgYzxMNz/r5Qda2QtzjYG4DWP3EmKMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWsYLn/btsHgYzxMNz/r5Qda2QtzjYG4DWP3EmKMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWsYLn/btsHgYzxMNz/r5Qda2QtzjYG4DWP3EmKMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWsYLn%2FbtsHgYzxMNz%2Fr5Qda2QtzjYG4DWP3EmKMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;128&quot; data-filename=&quot;logo-green-black.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 예시 코드의 변수이름이 조금 이상하다고 느끼지 않는가?&lt;br /&gt;보통 이런 변수들은 전부 대문자로 집어넣는 것에 대비된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 대한 대답은 &lt;a href=&quot;https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables&quot;&gt;Nuxtjs Runtime Config&lt;/a&gt; 문서를 보면 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwzNT0/btsHb57WoBw/8KJr3LyEHv63MVIbJ77ckk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwzNT0/btsHb57WoBw/8KJr3LyEHv63MVIbJ77ckk/img.png&quot; data-alt=&quot;Nuxtjs Runtime Config 문서의 한 부분이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwzNT0/btsHb57WoBw/8KJr3LyEHv63MVIbJ77ckk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwzNT0%2FbtsHb57WoBw%2F8KJr3LyEHv63MVIbJ77ckk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1558&quot; height=&quot;512&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Nuxtjs Runtime Config 문서의 한 부분이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 상세하게 설명하자면, Nuxtjs를 Production 배포할 때 문제가 생긴다. 이 때에는 Nuxtjs가 process.env에 저장된 변수를 읽어오지 않는다.&lt;br /&gt;process.env 중 &lt;code&gt;NUXT_&lt;/code&gt; 로 시작하는 변수만 찾아 읽어오도록 설계되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제에서 &lt;code&gt;runtimeConfig.public.backendUrl&lt;/code&gt; 변수는 process.env.&lt;code&gt;NUXT_PUBLIC_BACKEND_URL&lt;/code&gt; 에서 읽어온다는 의미이다.&lt;br /&gt;만약 &lt;code&gt;runtimeConfig.secretKey&lt;/code&gt; 변수는 process.env.&lt;code&gt;NUXT_SECRET_KEY&lt;/code&gt;에서 읽어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 블로그를 둘러보며 runtimeConfig에 대한 설명은 있었지만 Production에서의 NamingConvention은 없어서 더욱 설명하게 되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 이부분에 대해 몰라 많이 해메었었다. process.env를 바로 쓸 수 없는 것, Production 배포할 때의 환경변수 설정도 많은 블로그에 설명이 되어있지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 블로그를 보며 알아가는 계기가 되었으면 한다.&lt;/p&gt;</description>
      <category>TypeScript/Vuejs</category>
      <category>naming convention</category>
      <category>nuxtjs</category>
      <category>nuxtjs3</category>
      <category>runtimeconfig</category>
      <category>네이밍 컨벤션</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/5</guid>
      <comments>https://molasses-0.tistory.com/5#entry5comment</comments>
      <pubDate>Tue, 7 May 2024 19:22:17 +0900</pubDate>
    </item>
    <item>
      <title>Python FastAPI에서 ReCaptcha 토큰 검증</title>
      <link>https://molasses-0.tistory.com/4</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 &lt;a href=&quot;https://molasses-0.tistory.com/3&quot;&gt;Nuxt3 google ReCaptcha 사용하기&lt;/a&gt; 글의 연장이다.&lt;br /&gt;&lt;a href=&quot;https://correction.mori.space/&quot;&gt;맞춤법 검사기&lt;/a&gt; 를 구성하며 Nuxtjs 3와 FastAPI로 웹개발을 한 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 이야기는 Nuxt3에서 받아온 ReCaptcha Token을 FastAPI에서 검증하는 방법에 대한 이야기다. 같은 방법으로 Django나 Node.js 등 다양한 언어에서 적용이 가능하니 참고하기 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;timeline-enterprise@2x.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCCZOs/btsHh91DiGK/MVEqkDkhw9XDKLzN1nOZ6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCCZOs/btsHh91DiGK/MVEqkDkhw9XDKLzN1nOZ6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCCZOs/btsHh91DiGK/MVEqkDkhw9XDKLzN1nOZ6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCCZOs%2FbtsHh91DiGK%2FMVEqkDkhw9XDKLzN1nOZ6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;302&quot; data-filename=&quot;timeline-enterprise@2x.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT의 도움을 조금 받았다. (아니 사실 엄청 많이 받았다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 HTTP Request를 양식에 맞춰 보내는 것만으로도 ReCaptcha 토큰의 검증은 끝난다.&lt;br /&gt;아래는 FastAPI에서 실행시키기 위한 간단한 코드다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import HTTPException
import httpx


async def recaptcha_handler(recaptcha_response: str) -&amp;gt; bool:
    url = 'https://www.google.com/recaptcha/api/siteverify'
    data = {
        'secret': RECAPTCHA_SECRET_KEY,
        'response': recaptcha_response,
    }

    async with httpx.AsyncClient() as client:
        response = await client.post(url, data=data)
    result = response.json()

    if not result.get('success', False):
        raise HTTPException(status_code=500, detail='reCAPTCHA verification failed')
    return True
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI의 AsyncIO를 적절히 이용하기 위해 AsyncIO를 지원하는 httpx 라이브러리를 사용하는 모습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url에 발급받은 SECRET_KEY와 response token(클라이언트에서 넘어온 Token 이야기다!)을 POST 요청하면 &lt;code&gt;{ 'success': Bool }&lt;/code&gt; 형식으로 데이터가 넘어온다. 다른 데이터가 더 있을지도 모르지만 우리에게 중요한 필드는 &lt;code&gt;'success'&lt;/code&gt; 필드다.&lt;br /&gt;200 OK가 아닌 다른 데이터가 넘어오는 것에 대한 대응을 위해 &lt;code&gt;Dict.get()&lt;/code&gt;을 활용한다. 이 method의 두번째 인자는 필드를 찾지 못했을 때의 기본값이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 HTTP 요청만으로 ReCaptcha에 대한 검증을 바로 할 수 있다.&lt;br /&gt;심지어 ReCaptcha V3를 이용한다면 의심 유저에게만 캡챠를 진행할 수 있어 편리하고 유저 친화적인 웹 프로그래밍이 가능할 것이다.&lt;/p&gt;</description>
      <category>Python/FastAPI</category>
      <category>FastAPI</category>
      <category>recaptcha</category>
      <category>recaptcha token</category>
      <category>recaptcha 검증</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/4</guid>
      <comments>https://molasses-0.tistory.com/4#entry4comment</comments>
      <pubDate>Tue, 7 May 2024 16:42:30 +0900</pubDate>
    </item>
    <item>
      <title>Nuxt3 google ReCaptcha 사용하기</title>
      <link>https://molasses-0.tistory.com/3</link>
      <description>&lt;h3&gt;서론&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://correction.mori.space/&quot;&gt;맞춤법검사기&lt;/a&gt;를 만들면서 CSRF 인증 대신 더욱 안전한 ReCaptcha v3를 사용하기로 결정했다.&lt;br&gt;이 글은 Nuxt3에서 ReCaptcha 인증을 하는 방법에 대해 설명한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;timeline-enterprise@2x.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cwpgf/btsHisGCXFR/bbgdBBAFtpV54EBv3emqKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cwpgf/btsHisGCXFR/bbgdBBAFtpV54EBv3emqKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cwpgf/btsHisGCXFR/bbgdBBAFtpV54EBv3emqKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCwpgf%2FbtsHisGCXFR%2FbbgdBBAFtpV54EBv3emqKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;302&quot; data-filename=&quot;timeline-enterprise@2x.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;본론&lt;/h3&gt;
&lt;p&gt;우선 ReCaptcha SITE_KEY와 SECRET_KEY를 발급받았다는 전제 하에 진행한다.&lt;br&gt;&lt;a href=&quot;https://www.google.com/recaptcha/about/&quot;&gt;ReCaptcha ADMIN CONSOLE&lt;/a&gt;에서 간단히 발급받을 수 있다.&lt;/p&gt;
&lt;p&gt;npm i -s vue-recaptcha 로 필요한 &lt;a href=&quot;https://www.npmjs.com/package/vue-recaptcha&quot;&gt;vue-recaptcha&lt;/a&gt; 라이브러리를 받아준다.&lt;/p&gt;
&lt;p&gt;Nuxtjs 3의 &lt;code&gt;/plugins/recaptcha.ts&lt;/code&gt; 에 다음 코드를 작성한다. &lt;a href=&quot;https://dev.to/fitrakun/integrating-nuxt-3-with-recaptcha-v3-for-token-handling-2dp0&quot;&gt;dev.to 의 글&lt;/a&gt;을 참고했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineNuxtPlugin} from &amp;quot;#app&amp;quot;
import {VueReCaptcha} from &amp;quot;vue-recaptcha-v3&amp;quot;;

export default defineNuxtPlugin((nuxtApp) =&amp;gt; {
    const config = useRuntimeConfig()
    nuxtApp.vueApp.use(VueReCaptcha, {
        siteKey: config.public.recaptchaSiteKey ?? &amp;quot;&amp;quot;,
        loaderOptions: {
            autoHideBadge: false,
            explicitRenderParameters: {
                badge: &amp;#39;bottomright&amp;#39;,
            },
        },
    });
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기에서 &lt;code&gt;config.public.recaptchaSiteKey&lt;/code&gt; 변수는 &lt;code&gt;nuxt.config.ts&lt;/code&gt; 에서 설정해야 하는 변수이다. 나는 Dotenv 파일을 이용해 테스트 환경에서 변수를 설정해준다. 이 부분에 대한 내용은 나중에 블로그에 남길 예정이다.&lt;/p&gt;
&lt;p&gt;추가로 &lt;code&gt;/composables/useGoogleRecaptcha.ts&lt;/code&gt; 파일을 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {useReCaptcha} from &amp;quot;vue-recaptcha-v3&amp;quot;;

export default () =&amp;gt; {
  const recaptchaInstance = useReCaptcha()
  const executeRecaptcha = async (action: string) =&amp;gt; {
    await recaptchaInstance?.recaptchaLoaded()
    const token = await recaptchaInstance?.executeRecaptcha(action)
    const headerOptions = { headers: { &amp;#39;google-recaptcha-token&amp;#39;: token } }
    return { token, headerOptions }
  }

  return { executeRecaptcha }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;useGoogleRecaptcha 객체를 만드는 과정이다. 이 객체가 &amp;quot;ReCaptcha&amp;quot;를 사용할 때 중요한 함수이다.&lt;/p&gt;
&lt;h3&gt;사용방법&lt;/h3&gt;
&lt;p&gt;사용해야 하는 Vue components에 다음과 같이 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&amp;#39;ts&amp;#39;&amp;gt;
import useGoogleRecaptcha from &amp;quot;~/composables/useGoogleRecaptcha&amp;quot;;

const { executeRecaptcha } = useGoogleRecaptcha()

const { token } = await executeRecaptcha(&amp;#39;action&amp;#39;)
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기의 &lt;code&gt;token&lt;/code&gt; 변수에 유저의 리캡챠 검증용 토큰이 담겨있다.&lt;br&gt;&lt;code&gt;&amp;#39;action&amp;#39;&lt;/code&gt; 부분엔 ReCaptcha Console에서 Metric label로 사용할 action 이름을 적으면 된다.&lt;/p&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;이전의 Nuxt2에서 사용하던 &lt;a href=&quot;https://www.npmjs.com/package/@nuxtjs/recaptcha&quot;&gt;@nuxtjs/recaptcha&lt;/a&gt; 모듈은 Nuxt3에서 사용할 수 없다.&lt;br&gt;새로이 플러그인과 composable 코드를 작성해 코드를 구성해야 한다. 또한 검증용 토큰을 백엔드로 보내 백엔드에서 요청에 대한 검증도 같이 진행할 수 있다. 이 부분은 &lt;a href=&quot;https://blog.mori.space/4&quot;&gt;Python FastAPI를 사용한 내용&lt;/a&gt;을 참고하면 된다.&lt;/p&gt;</description>
      <category>TypeScript/Vuejs</category>
      <category>nuxt recaptcha</category>
      <category>nuxt3</category>
      <category>nuxtjs3</category>
      <category>recaptcha</category>
      <category>VUE</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/3</guid>
      <comments>https://molasses-0.tistory.com/3#entry3comment</comments>
      <pubDate>Tue, 7 May 2024 16:18:10 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI에서 Huggingface Transformer API를 사용해보자</title>
      <link>https://molasses-0.tistory.com/2</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞춤법검사기를 제작하면서 Huggingface Transformer API(이하 Transfomer API)를 FastAPI 백엔드에서 사용할 일이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 당연하게도 이런식으로 구성했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@router.post(&quot;/correction&quot;, response_model=CorrectionResponse)
@limiter.limit(&quot;60/seconds&quot;)
async def correction(request: Request, correction: CorrectionRequest, user: Session = Depends(get_logged_user)):
    # 1. Tokenize and pad inputs in batches
    batch = [tokenizer([f&quot;{tokenizer.bos_token}{p}{tokenizer.eos_token}&quot;for p in batch], add_special_tokens=True, padding=True, truncation=True, return_tensors=&quot;pt&quot;,
                       max_length=128) for batch in batches(contents, batch_size)]

    # 2. Initialize empty list for storing generated texts
    generated_texts = []

    # 3. Process batches efficiently using a loop
    for batch_inputs in batch:
        batch_outputs = model.generate(batch_inputs['input_ids'].to(device), max_length=128)
        batch_generated_texts = [tokenizer.decode(output, skip_special_tokens=True) for output in batch_outputs]
        generated_texts.extend(batch_generated_texts)
    return CorrectionResponse(content=generated_texts)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clPeXX/btsHgfhk9Ud/Ea00I5yEnUpcmyW7yo8cq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clPeXX/btsHgfhk9Ud/Ea00I5yEnUpcmyW7yo8cq0/img.png&quot; data-alt=&quot;huggingface logo&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clPeXX/btsHgfhk9Ud/Ea00I5yEnUpcmyW7yo8cq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclPeXX%2FbtsHgfhk9Ud%2FEa00I5yEnUpcmyW7yo8cq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1926&quot; height=&quot;512&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;huggingface logo&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;본론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;POST /correction&lt;/code&gt; 을 호출할 때마다 다른 백엔드 URL에는 제대로 접속이 되지 않는 현상이 발생한다.&lt;br /&gt;이유는 FastAPI에 있다.&lt;br /&gt;Async IO를 이용하는 FastAPI에서는 이런식으로 오래 걸리는 작업을 백엔드 함수에서 바로 실행한다면 다른 요청에는 제대로 응답하지 못하는 문제가 생길 수 있다. 아마 ASGI를 이용하는 Django에서도 이러한 문제가 생길 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 쓰는 asyncio의 이벤트 루프를 이용하자!&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;loop = asyncio.get_running_loop()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구문을 이용하면 loop 변수에는 지금 루프가 아닌 새로운 이벤트루프를 가져올 수 있다. 자세한 내용은 &lt;a href=&quot;https://docs.python.org/ko/3/library/asyncio-eventloop.html#asyncio.get_running_loop&quot;&gt;여기&lt;/a&gt;를 참고하길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 루프 안에서 Long job를 실행시키자!&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;result = await loop.run_in_executor(None, run_model, correction.content)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 사용법은 &lt;a href=&quot;https://docs.python.org/ko/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor&quot;&gt;여기&lt;/a&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 것이 &lt;a href=&quot;https://correction.mori.space/&quot;&gt;맞춤법 검사기&lt;/a&gt;다.&lt;/p&gt;</description>
      <category>Python/FastAPI</category>
      <category>asyncio</category>
      <category>FastAPI</category>
      <category>long job</category>
      <category>Python</category>
      <author>dalbodeule</author>
      <guid isPermaLink="true">https://molasses-0.tistory.com/2</guid>
      <comments>https://molasses-0.tistory.com/2#entry2comment</comments>
      <pubDate>Tue, 7 May 2024 15:49:36 +0900</pubDate>
    </item>
  </channel>
</rss>